RReverser / serde-xml-rs

xml-rs based deserializer for Serde (compatible with 1.0+)
https://crates.io/crates/serde-xml-rs
MIT License
270 stars 90 forks source link

Members of flattened structs are not converted #137

Open allsey87 opened 4 years ago

allsey87 commented 4 years ago

The following working example reproduces the problem. In a nutshell, there seems to be a problem with converting strings to the types inside struct Inner but only when it is flattened into struct Outer. These flattened members are unable to be parsed into booleans, integers etc.

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Inner {
    left: i32,
    right: i32, 
}

#[derive(Debug, Deserialize)]
struct Outer {
    name: String,

    #[serde(flatten)]
    inner: Inner,
}

fn main() {
    let pass = r#"<inner left="1" right="2" />"#;
    let fail = r#"<outer name="foo" left="1" right="2" />"#;

    println!("{:?}", serde_xml_rs::from_str::<Inner>(&pass));
    println!("{:?}", serde_xml_rs::from_str::<Outer>(&fail));
}

This prints:

Ok(Inner { left: 1, right: 2 })
Err(Custom("invalid type: string \"1\", expected i32"))

Deserialization of struct Outer only works when left and right are String. In this case, the output is:

Ok(Inner { left: "1", right: "2" })
Ok(Outer { name: "foo", inner: Inner { left: "1", right: "2" } })
tobz1000 commented 4 years ago

I believe the problem is due to the way a struct with a flatten attribute is deserialised. The derived implementation will:

One workaround is to use a custom deserializer for each field of the inner type:

#[derive(Debug, Deserialize)]
struct Inner {
    #[serde(deserialize_with = "string_as_i32")]
    left: i32,
    #[serde(deserialize_with = "string_as_i32")]
    right: i32,
}

#[derive(Debug, Deserialize)]
struct Outer {
    name: String,

    #[serde(flatten)]
    inner: Inner,
}

struct StringAsI32Visitor;

impl<'de> Visitor<'de> for StringAsI32Visitor {
    type Value = i32;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("integer value string")
    }

    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        value
            .parse()
            .map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
    }
}

fn string_as_i32<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i32, D::Error> {
    deserializer.deserialize_string(StringAsI32Visitor)
}
Matthias-Fauconneau commented 4 years ago

Since that was one of the issues which lead me to write a specialized XML deserializer, I thought I might mention it here. My solution was to use #[serde(rename="")] to pass flags like flatten to be interpreted by my custom XML deserializer. Here is an example specifying MusicXML. https://github.com/Matthias-Fauconneau/musicxml/blob/master/src/music_xml.rs

tobz1000 commented 4 years ago

Previous bug: #112

This issue is also present in the quick-xml crate: discussion

tobz1000 commented 4 years ago

I think i've found a suitable workaround for this issue. I've created a deserialize_with function which should be applied to all primitive fields in the types being deserialized. It will attempt to deserialize as normal first, and then as a string. It will then parse the string into a char, number, or bool (with the same rules as serde-xml-rs):

use crate::deserialize_with::flattened_xml_attr;

struct Foo {
    // Easiest to just apply it to every primitive, but you could be more selective
    #[serde(deserialize_with = "flattened_xml_attr")]
    a: i32,
    b: Bar,
    #[flatten]
    c: Baz
}

struct Bar {
    #[serde(deserialize_with = "flattened_xml_attr")]
    d: i32,
    e: String,
    #[serde(deserialize_with = "flattened_xml_attr")]
    f: bool
}

struct Baz {
    #[serde(deserialize_with = "flattened_xml_attr")]
    g: bool
}

It's just implemented in my consuming crate for now; no PRs for anywhere else. You can find it here to use as necessary.