Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling mixed object arrays in Serde

Expanding on my previous question, how do you handle an array that contains mixed structs that are both valid? I've tried looking at the serde_json::Value source. However it doesn't handle the case of two different structs.

I can't simply merge them, and use Options over their properties as that would make the single struct unwieldy, and it is important for them to be distinct.

Rust structs

#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
    foo: Vec<Structs>,
}

enum Structs {
    Foo(Foo),
    Bar(Bar),
}

#[derive(Clone, Debug, Deserialize)]
struct Foo {
    name: String,
    baz: Vec<String>,
}

#[derive(Clone, Debug, Deserialize)]
struct Bar {
    quux: u64
}

Example JSON

{
    "foo": [
        {
            "name": "John",
            "baz": ["Lorem", "Ipsum"]
        },
        {
            "quux": 17
        }
    ]
}
like image 759
XAMPPRocky Avatar asked Jun 26 '16 09:06

XAMPPRocky


Video Answer


1 Answers

There's a few ways to solve this. The easiest, if you have few variants, is to simply implement Deserialize manually like so:

impl serde::de::Deserialize for Structs {
    fn deserialize<D>(deserializer: &mut D) -> Result<Self, D::Error>
        where D: serde::Deserializer,
    {
        deserializer.deserialize(Visitor)
    }
}

struct Visitor;

impl serde::de::Visitor for Visitor {
    type Value = Structs;

    fn visit_map<V>(&mut self, mut visitor: V) -> Result<Structs, V::Error>
        where V: serde::de::MapVisitor,
    {
        let s: String = try!(visitor.visit_key()).expect("got struct with no fields");
        let val = match &s as &str {
            "name" => {
                Ok(Structs::Foo(Foo {
                    name: try!(visitor.visit_value()),
                    baz: {
                        let s: String = try!(visitor.visit_key()).expect("baz field");
                        assert_eq!(&s, "baz");
                        try!(visitor.visit_value())
                    },
                }))
            },
            "baz" => {
                Ok(Structs::Foo(Foo {
                    baz: try!(visitor.visit_value()),
                    name: {
                        let s: String = try!(visitor.visit_key()).expect("name field");
                        assert_eq!(&s, "name");
                        try!(visitor.visit_value())
                    },
                }))
            },
            "quux" => {
                Ok(Structs::Bar(Bar {
                    quux: try!(visitor.visit_value())
                }))
            },
            other => panic!("no struct has field `{}`", other),
        };
        try!(visitor.end());
        val
    }
}

The problem with this implementation is that it obviously doesn't scale. What you can do instead, is to create a new Deserializer that you give the first field name that was found and override the deserialize_map method to process the various structs through a custom MapVisitor.

If you feel that this is a common case supported by other serialization frameworks, feel free to post a bug report on the serde repository or the serde-json repository. I'm sure there's a way to automatically generate such an implementation, but it's not trivial for sure.

like image 179
oli_obk Avatar answered Oct 04 '22 19:10

oli_obk