Derive macros to deserialize an index enum from both a number and string

I have the following rust type:

enum SmallPrime {
    Two = 2,
    Three = 3,
    Five = 5,
    Seven = 7,
}

I can deserialize using json:

[ 2, 3, 5, 7 ]
use serde_repr::*;

#[derive(Copy, Clone, Serialize_repr, Deserialize_repr, PartialEq, Debug)]
#[repr(u8)]
enum SmallPrime {
    Two = 2,
    Three = 3,
    Five = 5,
    Seven = 7,
}

fn main() {
    use SmallPrime::*;
    
    // serialize o deserialize str -> [2,3,5,7]
    let j = r#"
        [ 2, 3, 5, 7 ]
    "#;
    let vs: Vec<SmallPrime> = serde_json::from_str(j).unwrap();
    println!("{}", serde_json::to_string(&vs).unwrap());

}

Does anyone know a way of simultaneously having it work with a "succeed first" strategy using an additional FromStr option such that the following might work?

// json
[ "Two", 3, "Five", "Seven" ]

... if not supporting both of these:

// json
[ "Two", "Three", "Five", "Seven" ]
[ 2, 3, 5, 7 ]

This can be achived using
#[serde(deserialize_with = "path")]`

Or you define an helper enum, using the untagged attribute
https://serde.rs/enum-representations.html

I'm on mobile now, but I can add later full examples if you want.

Edit: untagged example.Rust Playground

So, this may not be the shortest way to achieve what you want, but I like it being conceptually intuitive: you want your enum to be deserializable either in the default fashion, or with the repr flavor.

In Rust we can express "either" using an enum, and hide that very enum's disciminant from serde using untagged, as @MichaelV put it. This, thus, conceptually, yields:

#[derive(Deserialize)] // From "Two", "Three", _etc._
enum SmallPrimeNoRepr {
    Two = 2,
    Three = 3,
    Five = 5,
    Seven = 7,
}

#[derive(Deserialize_repr)] // From 2, 3, _etc._
#[repr(u8)]
enum SmallPrimeRepr {
    Two = 2,
    Three = 3,
    Five = 5,
    Seven = 7,
}

#[derive(Deserialize)]
#[serde(untagged)] // From either of the above
enum SmallPrime {
    NoRepr(SmallPrimeNoRepr),
    Repr(SmallPrimeRepr),
}

Now, your actual SmallPrime does not have that last shape, so now we need to express a conversion from the "conceptual serde shape" to you actual data structure. In other words, your actual SmallPrime enum needs to delegate to this helper of ours, and then be converted From it:

  #[derive(Deserialize)]
  #[serde(untagged)] // From either of the above
- enum SmallPrime {
+ enum SmallPrimeHelper {
      NoRepr(SmallPrimeNoRepr),
      Repr(SmallPrimeRepr),
  }

+ impl From<SmallPrimeHelper> for SmallPrime { … }

  #[derive(Serialize_repr, Debug, …)]
  #[repr(u8)]
- #[derive(Deserialize_repr)]
+ #[derive(Deserialize)]
+ #[serde(from = "SmallPrimeHelper")]
  enum SmallPrime {
      Two = 2,
      Three = 3,
      Five = 5,
      Seven = 7,
  }

and voilà!

3 Likes

Note that the process above can be automated by defining a helper macro. The following, for instance, compiles and runs fine:

#[macro_use]
extern crate macro_rules_attribute;

use ::serde_repr::{
    Serialize_repr,
};

#[derive(Copy, Clone, Serialize_repr, PartialEq, Debug)]
#[repr(u8)]
#[apply(derive_Deserialize_repr_or_not!)] // <- helper macro
enum SmallPrime {
    Two = 2,
    Three = 3,
    Five = 5,
    Seven = 7,
}

fn main ()
{   
    // serialize o deserialize str -> [2,3,5,7]
    let j = r#"
        [ 2, "Three", 5, 7 ]
    "#;
    let vs: Vec<SmallPrime> = serde_json::from_str(j).unwrap();
    println!("{}", serde_json::to_string(&vs).unwrap());
}
1 Like

The solutions using untagged seem quite complicated compared to a manual Deserialize implementation using visit_u64 and visit_str.

impl<'de> Deserialize<'de> for SmallPrime {
    fn deserialize<D: Deserializer<'de>>(d:D) -> Result<SmallPrime, D::Error> {
        struct Visitor;
        impl<'de> de::Visitor<'de> for Visitor {
            type Value = SmallPrime;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str("a prime as integer or text")
            }

            fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
                Ok(match value {
                    2 => SmallPrime::Two,
                    3 => SmallPrime::Three,
                    5 => SmallPrime::Five,
                    7 => SmallPrime::Seven,
                    _ => return Err(E::invalid_value(de::Unexpected::Unsigned(value), &self)),
                })
            }

            fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
                // A literal `FromStr` is also possible here
                Ok(match value {
                    "Two" => SmallPrime::Two,
                    "Three" => SmallPrime::Three,
                    "Five" => SmallPrime::Five,
                    "Seven" => SmallPrime::Seven,
                    _ => return Err(E::invalid_value(de::Unexpected::Str(value), &self)),
                })
            }
        }
        // Consider using deserialize_u64 or deserialize_str if used with a
        // non self describing data format
        d.deserialize_any(Visitor)
    }
}

https://www.rustexplorer.com/b/vs9fo8

You can, of course, use a macro, similar to what @Yandros posted, to generate both the enum and the Deserialize code.

2 Likes

These responses are so amazing; each really elegant in their own right.

Also, I have always wanted to "venture" going beyond conceptually understanding the mechanics of a custom macro to building something of use for my own code. @Yandros "beyond the call" - you have helped me "break the ice" with that gorgeous snippet of macro.

When it comes time to deserializing from a json resource, in my work, I often have to do so using "related but different" sources. This is where the "pre-processing" in other languages can be "a whole separate lift".

Not so in Rust and serde. By shifting how I model/think about the MyTypeRepr enum, it provides a type-safe, documented "how to funnel" a wide range of inputs into the "common-denominator" that motivated the work in the first place; it captures the "related, but different" task at hand.

This said, had I been more familiar/versed with how to "roll my own" deserializer, I would have likely gone with the "with" solution. This solution will be going into my snippets library; I need to better understand/intuit the deserialize + visitor pattern.