Search code examples
rustserdejson5

Variant types in JSON5 (serde)


I am wondering how one could support a "type-fluid" (aka variant type) field in JSON when mapped via serde. As parser, I am using json5 crate (backed by serde). And certain input fields might contain key-values which are either "foo": "bar" or a list behind the same key, i.e. "foo": [ "bar", "and", "more" ].

And the documentation of https://serde.rs/data-model.html does not actually give me a clue on how to map this usecase. I tried using an enum as variant carrier, see below, but that does not work, and at runtime this ends up in some error result like Err(Message { msg: "unknown variant `hello world`, expected `Single` or `Multi`", location: Some(Location { line: 4, column: 20 }) }).

So far I still don't see a way to tell it "take this either as value or as list of values and let me see which kind you have parsed".

#[derive(Deserialize, Debug, PartialEq)]
enum StringOrStrings {
    Single(String),
    Multi(Vec<String>),
}

#[derive(Deserialize, Debug, PartialEq)]
struct Config {
    message: StringOrStrings,
    n: i32,
}

Solution

  • You can use an untagged enum as the other answer explains, which allows you to deserialize one out of a few options, trying each in order until one succeeds.

    However, you can generalize this solution even further to handle any deserializable data type (not just strings), as well as hide the "maybe Vec" detail by wrapping a single value in a Vec like this:

    use serde::{Deserialize, Deserializer};
    
    #[derive(Deserialize, Debug)]
    struct Config {
        #[serde(deserialize_with = "deser_maybe_vec")]
        message: Vec<String>,
        n: i32,
    }
    
    fn deser_maybe_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
    where
        D: Deserializer<'de>,
        T: Deserialize<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum SingleOrVec<T> {
            Single(T),
            Vec(Vec<T>),
        }
    
        Ok(match SingleOrVec::deserialize(deserializer)? {
            SingleOrVec::Single(v) => vec![v],
            SingleOrVec::Vec(v) => v,
        })
    }
    

    Note that if you need to derive Serialize on Config you don't need to handle that case specially using serialize_with because the default implementation will produce a sequence containing one item, which is a valid representation of the data.