Search code examples
jsonrustserde

Deserialize an enum in Rust with both unit and non-unit versions without writing a custom deserializer


use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
enum Animal {
    Cat(Option<String>),
    Dog(String),
    Bird,
}

fn main() {
    let json_animals = r#"
        [
            {"Cat": "Meow"},
            "Cat",
            {"Dog": "Bark"},
            "Bird"
        ]"#;
    println!("{:?}", serde_json::from_str::<Vec<Animal>>(json_animals).unwrap());
}

We basically need a way to deserialize both "Cat" and {"Cat": "cat_name"} into Animal. I know that writing a custom deserializer works but it will be better to have a cleaner solution.

I tried aliasing & renaming & options & defining CatVariant as a separate enum to include both cases. None of these works because of Error("invalid type: unit variant, expected newtype variant", line: 5, column: 12)


Solution

  • You can do this without a custom Deserialize implementation, but it will require creating a few extra types to handle the various alternatives present in your schema.

    Since we can't directly make Animal implement Deserialize for your schema, we can create another type that represents that schema and then convert to Animal. #[serde(from = "OtherType")] will allow us to tell serde "deserialize OtherType and then convert it to this type."

    #[derive(Deserialize, Debug)]
    #[serde(from = "AnimalRepr")]
    enum Animal {
        Cat(Option<String>),
        Dog(String),
        Bird,
    }
    

    So what does AnimalRepr look like? Well, it's either a map or a string. We can used an untagged enum to represent that.

    #[derive(Deserialize, Debug)]
    #[serde(untagged)]
    enum AnimalRepr {
        Animal(AnimalType),
        BareAnimalType(BareAnimalType),
    }
    

    So now AnimalType will handle the maps and BareAnimalType will handle the strings.

    #[derive(Deserialize, Debug)]
    enum BareAnimalType {
        Cat,
        Bird,
    }
    
    #[derive(Deserialize, Debug)]
    enum AnimalType {
        Cat(String),
        Dog(String),
    }
    

    Now we just need a way to convert AnimalRepr to Animal. We can break this conversion apart by having both BareAnimalType and AnimalType convertible to Animal, and delegating to that conversion when converting AnimalRepr.

    impl From<BareAnimalType> for Animal {
        fn from(value: BareAnimalType) -> Self {
            match value {
                BareAnimalType::Cat => Self::Cat(None),
                BareAnimalType::Bird => Self::Bird,
            }
        }
    }
    
    impl From<AnimalType> for Animal {
        fn from(value: AnimalType) -> Self {
            match value {
                AnimalType::Cat(n) => Self::Cat(Some(n)),
                AnimalType::Dog(n) => Self::Dog(n),
            }
        }
    }
    
    impl From<AnimalRepr> for Animal {
        fn from(value: AnimalRepr) -> Self {
            match value {
                AnimalRepr::Animal(v) => v.into(),
                AnimalRepr::BareAnimalType(v) => v.into(),
            }
        }
    }
    

    Putting it all together, we get:

    use serde::{Deserialize, Serialize};
    
    #[derive(Deserialize, Debug)]
    enum BareAnimalType {
        Cat,
        Bird,
    }
    
    impl From<BareAnimalType> for Animal {
        fn from(value: BareAnimalType) -> Self {
            match value {
                BareAnimalType::Cat => Self::Cat(None),
                BareAnimalType::Bird => Self::Bird,
            }
        }
    }
    
    #[derive(Deserialize, Debug)]
    enum AnimalType {
        Cat(String),
        Dog(String),
    }
    
    impl From<AnimalType> for Animal {
        fn from(value: AnimalType) -> Self {
            match value {
                AnimalType::Cat(n) => Self::Cat(Some(n)),
                AnimalType::Dog(n) => Self::Dog(n),
            }
        }
    }
    
    #[derive(Deserialize, Debug)]
    #[serde(untagged)]
    enum AnimalRepr {
        Animal(AnimalType),
        BareAnimalType(BareAnimalType),
    }
    
    impl From<AnimalRepr> for Animal {
        fn from(value: AnimalRepr) -> Self {
            match value {
                AnimalRepr::Animal(v) => v.into(),
                AnimalRepr::BareAnimalType(v) => v.into(),
            }
        }
    }
    
    #[derive(Deserialize, Debug)]
    #[serde(from = "AnimalRepr")]
    enum Animal {
        Cat(Option<String>),
        Dog(String),
        Bird,
    }
    
    fn main() {
        let json_animals = r#"
            [
                {"Cat": "Meow"},
                "Cat",
                {"Dog": "Bark"},
                "Bird"
            ]"#;
        println!("{:?}", serde_json::from_str::<Vec<Animal>>(json_animals).unwrap());
    }
    

    Which outputs:

    [Cat(Some("Meow")), Cat(None), Dog("Bark"), Bird]
    

    (Playground)