XML to Rust: serde structs from a sample
In Rust, the usual way to read XML into typed data is serde together with an XML backend such as quick-xml (with its serialize feature) or serde-xml-rs. Both need structs that match the document. This XML to Rust generator writes those structs from a sample, with Serialize/Deserialize derives, snake_case fields and Rust types inferred from the element and attribute text.
Example
<crate name="serde" downloads="512000000">
<version num="1.0.210" yanked="false"/>
<version num="1.0.209" yanked="true"/>
<license>MIT OR Apache-2.0</license>
</crate>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Crate {
pub version: Vec<Version>,
pub license: String,
pub name: String,
pub downloads: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Version {
pub num: String,
pub yanked: bool,
}
The version string 1.0.210 stays a String (it is not a valid number), yanked became bool, and the repeated <version> elements became Vec<Version>.
Marking attributes for quick-xml
The generator does not know which fields were attributes. With quick-xml's serde support, attribute fields are renamed with an @ prefix and element text uses the special name $text:
#[derive(Debug, Deserialize)]
pub struct Crate {
#[serde(rename = "@name")]
pub name: String,
#[serde(rename = "@downloads")]
pub downloads: i64,
pub license: String,
#[serde(rename = "version", default)]
pub version: Vec<Version>,
}
let krate: Crate = quick_xml::de::from_str(&xml)?;
#[serde(default)] on the Vec lets documents without any <version> element still parse. For an element with attributes and text, add a field with #[serde(rename = "$text")]; the generator calls that field value.
How the sample is mapped
The root element name becomes the top-level struct (Crate), unless you enter your own. Attributes and child elements become fields of the same struct, element text beside attributes becomes a value field, and namespace prefixes are removed. Text is typed by content: 512000000 is an i64, false a bool, and anything else a String. Elements missing from some records become Option<T>, which quick-xml fills with None when they are absent.
Notes
- Rename conventions differ between XML crates;
serde-xml-rs, for example, does not use the@prefix. Check the documentation of the crate you choose. - Numeric text with leading zeros loses them when typed as a number; use
String. - Namespace prefixes are removed from field names.
For JSON data use JSON to Rust, and validate the XML with the XML Validator. Generation runs in your browser.
Writing XML back out
Because the structs derive Serialize as well, the same types can produce XML. With quick-xml, quick_xml::se::to_string(&krate)? writes the document, using the struct name as the root element. Two details usually need attention:
- The root name. The struct is called
Crate, but the document uses<crate>. Add#[serde(rename = "crate")]to the struct so the output matches the input. - Attributes versus elements. Fields without the
@rename are written as child elements, sonamewould become<name>serde</name>. Apply the renames from the section above before serializing if the receiver expects attributes.
Round-tripping a sample document, parse then serialize, is a quick test that the struct annotations are complete.