Deserializing a mix of tagged and untagged enum variants with Serde

I've got several enum variants that are represented as internally tagged JSON. I've also got a legacy variant that doesn't contain a tag field, but would like to be able to parse that into a variant too.

A minimal example might be (playground link):

use serde::Deserialize;
use serde_json;

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Thing {
    A { name: String },
    B { age: u32 },
    Legacy { count: u32 },
}

fn main() {
    let things: Vec<Thing> = serde_json::from_str(r#"
[
    {"type": "a", "name": "bob"},
    {"type": "b", "age": 50},
    {"count": 10}
]
"#).unwrap();
}

This of course panics with "missing field `type`".

So I'm basically looking for a way to deserialize to Thing::Legacy when the type field is missing (or somehow specify a default value of "legacy" for that field).

Any suggestions on how to achieve this using Serde?

The derives shipped with serde do not allow you to mix different tag representations for the same enum. One option is to split it into two enums. The first enum contains all tagged variants, the other the rest.

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum TaggedThing {
    A { name: String },
    B { age: u32 },
    Legacy { count: u32 },
}

#[derive(Deserialize)]
#[serde(untagged)]
enum Thing {
    Tagged(TaggedThing),
    Legacy { count: u32 },
}

Another option would be to only use untagged enums, and perform the tagging yourself.

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Thing {
    A {
        #[serde(rename = "type")]
        type_: monostate::MustBe!("a"),
        name: String,
    },
    B {
        #[serde(rename = "type")]
        type_: monostate::MustBe!("b"),
        age: u32,
    },
    Legacy {
        count: u32,
    },
}

serde also provides you the option to keep using your Thing struct and translate it from a better deserializable form (see above) when needed. Namely the from and into attributes of serde are relevant here.

2 Likes

Fantastic, both of those options look absolutely fine for my use case, thanks.

This solution is great into to perform tagging ourselves but when I tried your code, I get this element added to the resulting vector. How do I get rid of type_: MustBe!("a") and instead just have type: "a" or not have that field all together?

[A { type_: MustBe!("a"), name: "bob" }, B { type_: MustBe!("b"), age: 50 }, Legacy { count: 10 }]

This sounds like you want to use one type in your program and a different type definition for serialization purposes.