Is there a way to allow an unknown enum tag when deserializing with Serde?

Viewed 1126

I'm deserializing a tagged enum:

#[derive(Deserialize)]
enum Foo {
    A(A),
    B(B),
    C(C),
}

If Serde encounters a tag that isn't A, B or C, then it will throw an error. Is there any way to add a catch-all variant for unknown tags? I'd be happy if it only records the tag:

#[derive(Deserialize)]
enum Foo {
    A(A),
    B(B),
    C(C),
    #[serde(unknown_tag)]
    Unknown(String),
}
1 Answers

You can use an untagged enum for this. The details depend on what you want to do exactly. The idea is to wrap Foo into a MaybeFoo, where MaybeFoo has a "universal" type to deserialize into as the second choice.

In the example below, we use a serde_json::Value as a dummy-type, as its implementation of Deserialize is universal as can deserialize anything that is valid JSON. If your source format is different, you may need a different deserializer or implement Deserialize yourself.

#[derive(serde::Deserialize, serde::Serialize, PartialEq, Debug)]
enum Foo {
  A(u64),
  B(f32),
  C(String),
}

// MaybeFoo is untagged, which also means it "looks" exactly
// like a Foo when serialized/deserialized. 
#[derive(serde::Deserialize, PartialEq, Debug)]
#[serde(untagged)]
enum MaybeFoo {
    Foo(Foo),
    Other(serde_json::Value)
}

The MaybeFoo is an "untagged" enum and Serde will try to deserialize MaybeFoo as a Foo and - if that fails - as serde_json::Value which will always succeed (if sourced from JSON).

fn main() {
    // Lets create a Foo and serialize it
    let foo = Foo::B(0.0);
    let foo_json = serde_json::to_string(&foo).unwrap();
    println!("{}", &foo_json);

    // Deserialize works as expected
    let foo_json = "{\"B\":0.0}";
    assert!(serde_json::from_str::<Foo>(&foo_json).unwrap() == foo);

    // Deserializing as a `MaybeFoo` works as expected
    assert!(serde_json::from_str::<MaybeFoo>(&foo_json).unwrap() == MaybeFoo::Foo(foo));    

    // Deserializing something else is not a `Foo`!
    let foo_json = "{\"Unknown\":0.0}";
    let foo = serde_json::from_str::<MaybeFoo>(&foo_json).unwrap();

    // Prints "Other(Object({"Unknown": Number(0.0)}))"
    println!("{:?}", &foo);
}

You can use serde_json's API to inspect the unknown variant and - if it looks like a map - extract the tag. If this is your only interest, the second variant of MaybeFoo could also be a HashMap<String, serde::de::IgnoredAny>, which will deserialize any map, record the tag as a String and throw away the value. This presumes, however, that the unknown value is a tagged value.

Related