How can I deserialize a fieldless enum from either a string or number?

Viewed 289

Is there a succinct way to deserialize a variant of a fieldless enum from either its name or discriminant value? e.g. given this enum—

enum Foo {
    A = 1,
    B = 2,
    C = 3,
}

—I’d like any of these strings or numbers to represent it:

{
  "example-a": "a", // Foo::A
  "example-b": "b", // Foo::B
  "example-c": "c", // Foo::C

  "example-1": 1, // Foo::A
  "example-2": 2, // Foo::B
  "example-3": 3, // Foo::C
}

I’ve seen that deriving Deserialize accommodates the former group and Deserialize_repr the latter, but I’m not sure how to accommodate both simultaneously.

I expected that a shorthand like #[serde(alias = …)] might exist to cover this scenario.

1 Answers

There is not a built-in shortcut that supports this directly. You will have to implement Deserialize manually. It's neither trivial nor super complicated:

impl<'de> serde::Deserialize<'de> for Foo {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>
    {
        struct FooVisitor;
        
        impl<'de> serde::de::Visitor<'de> for FooVisitor {
            type Value = Foo;
            
            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(formatter, "an integer or string representing a Foo")
            }
            
            fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Foo, E> {
                Ok(match s {
                    "a" => Foo::A,
                    "b" => Foo::B,
                    "c" => Foo::C,
                    _ => return Err(E::invalid_value(serde::de::Unexpected::Str(s), &self)),
                })
            }
            
            fn visit_u64<E: serde::de::Error>(self, n: u64) -> Result<Foo, E> {
                Ok(match n {
                    1 => Foo::A,
                    2 => Foo::B,
                    3 => Foo::C,
                    _ => return Err(E::invalid_value(serde::de::Unexpected::Unsigned(n), &self)),
                })
            }
        }
        
        deserializer.deserialize_any(FooVisitor)
    }
}

Note that using deserialize_any means we are relying on the data format being self-describing; i.e., that the deserializer knows whether the data is stringy or integerish and will call the correct visit_ method accordingly. Serde also supports non-self-describing formats; however, you won't be able to use them with this Deserialize implementation.

Related