Search code examples
xmlrustserde

Deserialising XML with name and type as attributes


I'm attempting to use use Serde and quick-xml to deserialise an XML document. However, the type of the element and the name of the item in the parent struct are both in XML attributes:

<Root>
    <Attribute Name="MAKE" Type="String">Ford</Attribute>
    <Attribute Name="INSURANCE_GROUP" Type="Integer">10</Attribute>
</Root>

I would like to deserialise that into this struct:

struct Root {
    make: String,
    insurance_group: u8,
}

I've tried using the tag attribute on the parent to specify that it should use "Type" as the object type, but I have no idea how to tell it to use "Name" as the variable name in the struct. Everything I've tried results in Err value: Custom("missing field MAKE")'.

This code should demonstrate the issue:

use quick_xml::de::from_str;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "Type", rename_all = "SCREAMING_SNAKE_CASE")] // What else do I add here to specify "Name"?
struct Root {
    make: String,
    insurance_group: u8,
}

fn main() {
    println!("Hello, world!");
    let xml = r#"<Root>
        <Attribute Name="MAKE" Type="String">Ford</Attribute>
        <Attribute Name="INSURANCE_GROUP" Type="Integer">10</Attribute>
    </Root>"#;
    let import: Root = from_str(xml).unwrap();
    dbg!(&import);
}

Ideally I would like to access the values directly using import.make (without needing to match an enum), but I realise this may not be feasible.


Solution

  • I don't see a way of convincing quick_xml to use an attribute as the key for a field. You can deserialize into:

    use serde_with::{serde_as, DisplayFromStr};
    
    #[serde_as]
    #[derive(Serialize, Deserialize, Debug)]
    #[serde(tag = "Name", rename_all = "SCREAMING_SNAKE_CASE")]
    enum Attribute {
        Make {
            #[serde(rename = "$value")]
            make: String,
        },
        InsuranceGroup {
            #[serde(rename = "$value")]
            #[serde_as(as = "DisplayFromStr")]
            insurance_group: u8,
        },
    }
    
    #[derive(Serialize, Deserialize, Debug)]
    #[serde(rename = "Root")]
    struct SerdeRoot {
        #[serde(rename = "Attribute")]
        attributes: Vec<Attribute>,
    }
    

    Now, if you insist on using your original data structure, you can additionally do something like

    use derive_builder::Builder;
    
    #[derive(Serialize, Clone, Deserialize, Debug, Builder)]
    #[serde(try_from = "SerdeRoot", into = "SerdeRoot")]
    struct Root {
        make: String,
        insurance_group: u8,
    }
    
    impl TryFrom<SerdeRoot> for Root {
        type Error = RootBuilderError;
        fn try_from(value: SerdeRoot) -> Result<Self, Self::Error> {
            let mut builder = RootBuilder::default();
            for a in value.attributes {
                match a {
                    Attribute::Make { make } => builder.make(make),
                    Attribute::InsuranceGroup { insurance_group } => {
                        builder.insurance_group(insurance_group)
                    }
                };
            }
            builder.build()
        }
    }
    

    which will ultimately make your from_str::<Root>(xml) work as desired. (You'd also need an Into<SerdeRoot> implementation and some extra fields for Type if you want to make serialization work, but that should be easy.)