quick-xml icon indicating copy to clipboard operation
quick-xml copied to clipboard

Serialization of enums adds extra tags

Open Storyyeller opened this issue 3 years ago • 5 comments

Currently, we have some types using quick_xml and serde to parse XML, which works well. However, we want to also serialize them back to XML, and the XML that quick_xml produces differs from the original (expected) format, and I can't figure out any way to fix it.

Our type definitions look like this:

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Contactinfo {
    pub bugurl: String,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Include {
    pub name: String,
    #[serde(default)]
    pub groups: String,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ManifestChild {
    Include(Include),
    Contactinfo(Contactinfo),

    #[serde(other)]
    Unknown,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Manifest {
    #[serde(default, rename = "$value")]
    pub children: Vec<ManifestChild>,
}

And we can parse XML with it just fine:

let text = r#"
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
  <include name="bar"/>
  <contactinfo bugurl="foo"/>
  <include name="hello" groups="world"/>
</manifest>"#;

    let result = quick_xml::de::from_str(text);
    let m: Manifest = result.unwrap();

    let s = quick_xml::se::to_string(&m).unwrap();
    println!("{}", s);

However, the output XML is in the wrong format, wrapping all the enum contents in an extra tag. How can we get rid of this and make it serialize in the same way as the input xml? Also, why is Manifest capitalized in the output when it isn't in the input? It seems like serialization ignores our #[serde(rename_all = "kebab-case")] annotations!

Here is what the output looks like with quick_xml 0.23alpha3:

<Manifest><include><Include name="bar"/></include><contactinfo><Contactinfo bugurl="foo"/></contactinfo><include><Include name="hello" groups="world"/></include></Manifest>

Storyyeller avatar Dec 17 '21 22:12 Storyyeller

Really need this to be fixed. Currently this is the best-working serializer for XML but it still doesn't cut the cake.

spikespaz avatar Jan 10 '22 00:01 spikespaz

So far the best workaround I came up with involved writing a combination of four wrapper types with hundreds of lines of boilerplate to change the way it serializes.

Storyyeller avatar Jan 10 '22 01:01 Storyyeller

@Storyyeller Would you mind sharing that code? Could really use it and want to save time.

spikespaz avatar Jan 10 '22 02:01 spikespaz

Seems like there was already a pill request that fixes this but it is made on a tree that is 199 commits ago.... Makes me a little annoyed because the reason the repo owner denied the PR is that they didn't "like" it.

https://github.com/tafia/quick-xml/pull/205

spikespaz avatar Jan 10 '22 08:01 spikespaz

It seems like serialization ignores our #[serde(rename_all = "kebab-case")] annotations!

The reason, why Manifest in XML was capitalized in that that rename_all annotation applied only to struct fields, but not to struct name, and root tag inferred from the top-level struct name that you trying to serialize.

Since 0.20.0 you can choose root tag name using with_root(name) constructor.

The other questions still need an investigation, I'll probably look at them after finishing proper deserializer support.

Mingun avatar May 25 '22 15:05 Mingun

@Storyyeller, when #490 will be merged, the following code will do what you want:


#[test]
fn issue346() {
    #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
    pub struct Contactinfo {
        #[serde(rename = "@bugurl")]
        pub bugurl: String,
    }

    #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
    pub struct Include {
        #[serde(rename = "@name")]
        pub name: String,

        #[serde(rename = "@groups", default)]
        #[serde(skip_serializing_if = "str::is_empty")]
        pub groups: String,
    }

    #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
    #[serde(rename_all = "kebab-case")]
    pub enum ManifestChild {
        Include(Include),
        Contactinfo(Contactinfo),

        #[serde(other)]
        Unknown,
    }

    #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
    #[serde(rename_all = "kebab-case")]
    #[serde(rename = "manifest")]
    pub struct Manifest {
        #[serde(default, rename = "$value")]
        pub children: Vec<ManifestChild>,
    }

    let text = r#"
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
  <include name="bar"/>
  <contactinfo bugurl="foo"/>
  <include name="hello" groups="world"/>
</manifest>"#;

    let result = quick_xml::de::from_str(text);
    let m: Manifest = result.unwrap();
    assert_eq!(m, Manifest {
        children: vec![
            ManifestChild::Include(Include { name: "bar".to_string(), groups: "".to_string() }),
            ManifestChild::Contactinfo(Contactinfo { bugurl: "foo".to_string() }),
            ManifestChild::Include(Include { name: "hello".to_string(), groups: "world".to_string() }),
        ]
    });

    assert_eq!(
        quick_xml::se::to_string(&m).unwrap(),
        "\
        <manifest>\
            <include name=\"bar\"/>\
            <contactinfo bugurl=\"foo\"/>\
            <include name=\"hello\" groups=\"world\"/>\
        </manifest>"
    )
}

Mingun avatar Oct 01 '22 21:10 Mingun