Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deserialize value that may be an array of strings or a constant string?

I am deserializing some JSON using Serde. I am having problems with a value that is usually an array of strings, but can also be the constant string "all". Expressed in JSON-schema it looks like this:

{
    "oneOf": [
        {
            "const": "all" 
        },
        {
            "type": "array",
            "items": {
                "type": "string"
            }
        }
    ]
}

I want to serialize it into this enum:

enum MyList {
    List(Vec<String>),
    All
}

Here are a few examples:

["foo", "bar"]  // Should be MyList::List
"all"           // Should be MyList::All
"foo"           // Should be an error!

The question is, how do I deserialize this enum using serde? None of the container attributes or variant attributes seem to help. If need be, I may change the design of the enum. However, the structure of the JSON is beyond my control.

like image 261
Anders Avatar asked Dec 17 '25 21:12

Anders


1 Answers

It's possible with the combination of an untagged enum representation and deserialize_with variant attribute:

use serde::{Deserialize, Deserializer};

#[derive(Debug, Deserialize)]
// This annotation "flattens" the enum, allowing one to treat a list of strings
// directly as a List variant, without extra annotations.
// Serde will attempt to deserialize input as each variant in order,
// returning an error if no one matches.
#[serde(untagged)]
enum MyList {
    // This annotation delegates deserialization of this variant to own code,
    // since otherwise Serde wouldn't know what to do with the string.
    #[serde(deserialize_with = "all")]
    All,
    // This variant is deserialized as usual.
    List(Vec<String>),
}

// A custom deserialization function, referred to by `deserialize_with`.
// (reference: https://github.com/serde-rs/serde/issues/1158)
fn all<'de, D>(deserializer: D) -> Result<(), D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize)]
    // This enum is, by default, "externally tagged";
    // but, since it consists only of a single unit variant,
    // it means that it can be deserialized only from
    // the corresponding constant string - and that's exactly what we need
    enum Helper {
        #[serde(rename = "all")]
        Variant,
    }
    // We're not interested in the deserialized value (we know what it is),
    // so we can simply map it to (), as required by signature
    Helper::deserialize(deserializer).map(|_| ())
}

fn main() {
    // Trying to parse an array with both variants...
    let json = r#"["all", ["a", "b", "c"]]"#;
    let lists: Vec<MyList> = serde_json::from_str(json).expect("cannot parse");
    // ...we can see that it is indeed parsed correctly...
    println!("{:?}", lists);

    // ...and any unexpected string results in an error
    serde_json::from_str::<MyList>(r#""foo""#).unwrap_err();
}

Playground

like image 95
Cerberus Avatar answered Dec 19 '25 13:12

Cerberus



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!