Serde: custom Deserialize


#1

I’m trying to implement serde’s Deserialize for a type. It is serialized similar to an enum, with a “tag” and a value, but the “tag” isn’t just a simple discriminant, but its own Deserialize-able type. Like with an enum, the interpretation of the following value depends on that tag. Is this possible with serde’s data model, and is there an example how to do this somewhere?


#2

Suppose you have a struct called Foo with fields bar and baz.
If Foo’s fields (bar and baz) are all Deserializable and Serializable, you can easily make Foo itself De/Serializable by simply deriving the traits:

#[derive(Deserialize, Serialize)]
struct Foo {
   bar : Bar,
   baz: Baz,
}

A similar story holds for enums: If all variants only have fields that are De/Serializable, then the enum can use the same derive attribute invocation as my example for structs.


#3

Sure, that’s the easy case. My problem is, in your example struct, baz is conceptually an enum whose variant is determined by bar (and in the end, I want to deserialize it into a single enum).


#4

Could you post an example? Perhaps it would help clear up the exact situation you’re facing.


#5

Well: let’s say I want to get this enum out:

enum Target {
    Foo { meta: String, content: String },
    Bar { meta: bool, content: u64 },
}

and the data looks like this:

[{"type": "Foo", "meta": "blah"}, "content string"]  # for a Foo
[{"type": "Bar", "meta": true},   12345]             # for a Bar

In reality the data is in a non-self-describing binary format, so I really have to decode the first part first to decide how to decode the second.


#6

I’m pretty sure you can build such a thing, especially if you know the order is always like this.

What I recommend is not directly parsing into:

enum Target {
    Foo { meta: String, content: String },
    Bar { meta: bool, content: u64 },
}

But instead into:

enum Tag {
    Foo { meta: String },
    Bar { meta: bool },
}

enum Data {
    Text(String),
    Number(u64)
}

enum Target {
    Foo { meta: String, content: String },
    Bar { meta: bool, content: u64 },
}

impl From<(Tag, Data)> for Target....

That will make the development of the Deserializer much easier and straight-forward (as Tag can be fully constructed before Data) and mapping between the two structures should be very cheap.

I hope that gives you a sense of direction.


#7

Thanks. I know it must look similar to how an enum is deserialized, just that the tag is more complex.

There is an example “deserialize a Struct” that shows what derive emits for a struct, but not for an enum.

Is there an easy way to see the generated code for #[derive(Deserialize)]? I know I can modify the crate and dump the TokenStream, but…


#8

Not that I know of, but take a look at the rustdocs for Deserialize, Deserializer and Visitor.
All methods that can be implemented are listed there, including variants for enums.


#9

@dtolnay any insights?


#10

@birkenfeld Had some time at hand. Here’s a implementation of the scenario you describe above using the technique I presented above (sans the From implementation and some error handling, like enforcing the length of the array to be 2, it also panics instead of erroring).

https://play.rust-lang.org/?gist=79b0423802920c610f441e64ed729687&version=stable&mode=debug

If you absolutely need to, you can avoid the From step, I think the path to do that is visible.


#11

Thanks, that works indeed, and makes the use of the Visitors clearer for me.


#12

I would write this as: playground.

fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
    A: SeqAccess<'de>,
{
    let tag = match seq.next_element()? {
        Some(tag) => tag,
        None => return Err(de::Error::custom("missing wacky tag")),
    };

    match tag {
        Tag::Foo { meta } => match seq.next_element()? {
            Some(content) => Ok(Target::Foo { meta, content }),
            None => Err(de::Error::custom("missing Foo content")),
        }
        Tag::Bar { meta } => match seq.next_element()? {
            Some(content) => Ok(Target::Bar { meta, content }),
            None => Err(de::Error::custom("missing Bar content")),
        }
    }
}

#13

Thanks!