rustserdeserde-json

Deserialization of optionally-wrapped enum


I have a Rust web service endpoint which accepts a JSON payload. The payload contains a nested enum structure, something like:

    #[derive(Serialize, Deserialize, Debug, PartialEq)]
    #[serde(rename_all = "snake_case")]
    enum InnerEnum {
        ValueA(String),
        ValueB
    }

    #[derive(Serialize, Deserialize, Debug, PartialEq)]
    #[serde(rename_all = "snake_case")]
    enum OuterEnum {
        Wrapped(InnerEnum),
        Other
    }

    #[derive(Serialize, Deserialize, Debug, PartialEq)]
    struct Message {
        message: OuterEnum
    }

Serialization is cake, and this works great with a payload like { "message": { "wrapped": { "value_a": "foo" } } }, but older versions of this API didn't do the snake case renaming and didn't wrap InnerEnum with OuterEnum, so older clients are prone to call with payloads like { "message": { "ValueA": "foo" } }

I want to support those old clients without exposing multiple versions of the API or maintaining multiple versions of the endpoint, by allowing either payload to be deserialized to the current struct.

So far I've tried using #[serde(deserialize_with = "...")] on the message field to call a function like

    fn enum_deserializer<'de, D>(de: D) -> Result<OuterEnum, D::Error> where D: serde::Deserializer<'de> {
        if let Ok(inner) = InnerEnum::deserialize(de) {
            Ok(OuterEnum::Wrapped(inner))
        } else {
            OuterEnum::deserialize(de)
        }
    }

but the deserialize call consumes the deserializer so I cannot call it twice. I've tried building a custom deserializer for OuterEnum, but I cannot figure out how to deserialize the field to a map in a generic way, or to peek at the enum tag before deserializing. I've even tried just using #[serde(other)] on OuterEnum::Other as a fallback, but apparently that requires both that the enum variant be a unit type and that the input json field be a simple string and not a map/array/etc. Is there a reasonable way to achieve this?


Solution

  • What I normally do in these situations is use some extra helper structs. Combined with heavy usage of #[serde(alias)], you should be able to handle the various legacy formats:

    use serde::{Serialize, Deserialize, de::Deserializer};
    
    #[derive(Serialize, Deserialize, Debug, PartialEq)]
    enum InnerEnum {
        #[serde(rename = "value_a")]
        #[serde(alias = "ValueA")]
        ValueA(String),
        #[serde(rename = "value_b")]
        #[serde(alias = "ValueB")]
        ValueB
    }
    
    #[derive(Serialize, Deserialize, Debug, PartialEq)]
    enum OuterEnum {
        #[serde(rename = "wrapped")]
        #[serde(alias = "Wrapped")]
        Wrapped(InnerEnum),
        #[serde(rename = "other")]
        #[serde(alias = "Other")]
        Other
    }
    
    #[derive(Deserialize, Debug, PartialEq)]
    #[serde(untagged)]
    enum LegacyOuterEnum {
        #[serde(rename = "wrapped")]
        #[serde(alias = "Wrapped")]
        Wrapped(InnerEnum),
        #[serde(rename = "other")]
        #[serde(alias = "Other")]
        Other
    }
    
    #[derive(Deserialize, Debug, PartialEq)]
    #[serde(untagged)]
    enum MessageHelper {
        Normal {
            message: OuterEnum,
        },
        Legacy {
            message: LegacyOuterEnum,
        },
    }
    
    #[derive(Serialize, Debug, PartialEq)]
    struct Message {
        message: OuterEnum,
    }
    
    impl<'de> Deserialize<'de> for Message {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: Deserializer<'de>,
        {
            Ok(match <MessageHelper as Deserialize>::deserialize(deserializer)? {
                MessageHelper::Normal { message } => Message { message },
                MessageHelper::Legacy { message } => Message {
                    message: match message {
                        LegacyOuterEnum::Wrapped(i) => OuterEnum::Wrapped(i),
                        LegacyOuterEnum::Other => OuterEnum::Other,
                    }
                }
            })
        }
    }
    
    fn main() {
        dbg!(serde_json::from_str::<Message>(r##"{ "message": { "wrapped": { "value_a": "foo" } } }"##));
        dbg!(serde_json::from_str::<Message>(r##"{ "message": { "ValueA": "foo" } }"##));
    }
    

    playground