How can I deserialize an enum with an optional internal tag?
Asked Answered
J

2

6

I use Serde to deserialize a custom configuration file written in YAML. The file can contain definitions of various kinds that I represent as internally tagged enums:

OfKindFoo:
  kind: Foo
  bar: bar;
  baz: baz;

OfKindQux:
  kind: Qux
  quux: qux;

I represent it in Rust like this:

#[derive(Deserialize)]
#[serde(tag = "kind")]
enum Definition {
    Foo(Foo),
    Qux(Qux),
}

#[derive(Deserialize)]
struct Foo {
    bar: String,
    baz: String,
}

#[derive(Deserialize)]
struct Qux {
    quux: String,
}

I want the user to be able to omit the kind field completely, and when it is omitted Serde should default to deserializing it as Foo.

I started to implement Deserialize on Definition. I'm trying to deserialize it as a map and look for the kind key and return a respective enum variant based on this key and whether it is present.

I need to somehow "forward" the deserialization of other map fields to Foo::deserialize or Bar::deserialize, respectively. fn deserialize only takes one argument which is Deserializer. Is there a way to "convert" the map into a deserializer or otherwise get a deserializer that "starts" on that particular map?

I cannot use #[serde(other)] because it returns Err for a missing tag. Even if it didn't, the documentation states that other can only be applied to a "unit variant", a variant not containing any data.

Jennefer answered 14/4, 2020 at 20:34 Comment(0)
A
9

You can mark the main enum as untagged and add tags to the sub-structs that do have a tag (this feature is not documented, but was added deliberately and so seems likely to stay). The variant without a tag should be declared after the other ones though, as serde will try to deserialize the variants in declared order with #[serde(untagged)]. Also note that if in your actual code, the variants and the structs have different names, or you're using #[serde(rename)], with this, the names of the structs are what matters for (de)serialization, not the variant names. All that applied to your example:

#[derive(Deserialize)]
#[serde(untagged)]
enum Definition {
    Qux(Qux),
    Foo(Foo), // variant that doesn't have a tag is the last one
}

#[derive(Deserialize)]
struct Foo {
    bar: String,
    baz: String,
}

#[derive(Deserialize)]
#[serde(tag = "kind")]
// if you want the tag to be "qux" instead of "Qux", do
// #[serde(rename = "qux")]
// here (or add `, rename = "qux"` to the previous serde attribute)
struct Qux {
    quux: String,
}
Allegory answered 15/4, 2020 at 0:11 Comment(1)
As @seyed explains in his answer below, this does not work if multiple structs have an identical required structure (can also happen e.g. due to defaults for many fields).Janellajanelle
M
1

If structs have the same shape or all the fields are optional, accepted answer won't work and will be deserialized to the first kind. playground

With the monostate crate it can be fixed.


use monostate::MustBe;
use serde::{Deserialize, Serialize};

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Action {
    Hi(Hi),
    Bye(Bye),
    Unknown(Unknown),
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Hi {
    kind: MustBe!("Hi"),
    name: String,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Bye {
    kind: MustBe!("Bye"),
    name: String,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Unknown {
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tests() {
        assert_eq!(
            Action::Hi(Hi { kind: Default::default(),  name: "John".to_string() }),
            serde_json::from_str::<Action>(r#"{"kind": "Hi", "name": "John"}"#).unwrap()
        );
        assert_eq!(
            Action::Bye(Bye { kind: Default::default(), name: "John".to_string() }),
            serde_json::from_str::<Action>(r#"{ "kind": "Bye", "name": "John" }"#).unwrap()
        );
        assert_eq!(
            Action::Unknown(Unknown {  }),
            serde_json::from_str::<Action>(r#"{ "name": "John" }"#).unwrap()
        );
        assert_eq!(
            Action::Unknown(Unknown {  }),
            serde_json::from_str::<Action>(r#"{}"#).unwrap()
        );
    }

}


Murex answered 23/11, 2022 at 9:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.