13 releases (breaking)
Uses new Rust 2024
| new 0.44.0 | Mar 1, 2026 |
|---|---|
| 0.42.0 | Jan 6, 2026 |
| 0.41.0 | Dec 31, 2025 |
#2511 in Parser implementations
Used in 7 crates
(5 directly)
300KB
5K
SLoC
facet-xml
facet-xml
XML serialization and deserialization for Rust using the facet reflection framework.
The XML serializer and deserializer assumes every node is lowerCamelCase, unless explicitly renamed.
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
struct Banana {
taste: String,
}
# let xml_str = "<banana><taste>sweet</taste></banana>";
# let b: Banana = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(b.taste, "sweet");
Use rename to override:
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
#[facet(rename = "Banana")]
struct Banana {
taste: String,
}
# let xml_str = "<Banana><taste>sweet</taste></Banana>";
# let b: Banana = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(b.taste, "sweet");
Child Elements
By default, fields are matched against child elements with the same name (in lowerCamelCase).
<person>
<name>Ella</name>
<age>42</age>
</person>
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
struct Person {
name: String, // captures "Ella"
age: u32, // captures 42
}
# let xml_str = "<person><name>Ella</name><age>42</age></person>";
# let person: Person = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(person.name, "Ella");
# assert_eq!(person.age, 42);
Attributes
Use xml::attribute to capture XML attributes:
<link href="/home">Home</link>
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
struct Link {
#[facet(xml::attribute)]
href: String, // captures "/home"
#[facet(xml::text)]
text: String, // captures "Home"
}
# let xml_str = r#"<link href="/home">Home</link>"#;
# let link: Link = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(link.href, "/home");
# assert_eq!(link.text, "Home");
Text
Use xml::text to capture text content:
<name>Ella</name>
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
struct Name {
#[facet(xml::text)]
value: String, // captures "Ella"
}
# let xml_str = "<name>Ella</name>";
# let name: Name = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(name.value, "Ella");
Lists
For list types (Vec, etc.), facet-xml collects items. By default, items are child elements with the singularized field name (via facet-singularize).
Default: child elements with singularized name
<playlist>
<track>Song A</track>
<track>Song B</track>
</playlist>
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
struct Playlist {
tracks: Vec<String>, // "tracks" → expects <track> elements
}
# let xml_str = "<playlist><track>Song A</track><track>Song B</track></playlist>";
# let playlist: Playlist = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(playlist.tracks, vec!["Song A", "Song B"]);
Override element name with rename
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
struct Playlist {
#[facet(rename = "song")]
tracks: Vec<String>, // expects <song> instead of <track>
}
# let xml_str = "<playlist><song>Song A</song><song>Song B</song></playlist>";
# let playlist: Playlist = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(playlist.tracks, vec!["Song A", "Song B"]);
Explicit xml::elements (same as default)
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
struct Playlist {
#[facet(xml::elements)]
tracks: Vec<String>,
}
# let xml_str = "<playlist><track>Song A</track><track>Song B</track></playlist>";
# let playlist: Playlist = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(playlist.tracks, vec!["Song A", "Song B"]);
Lists of structs with rename
When collecting struct items, use rename to specify the element name. The rename overrides
the default singularized field name:
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug, PartialEq)]
struct Person {
#[facet(xml::attribute)]
name: String,
}
#[derive(Facet, Debug)]
struct Team {
// "individual" instead of default "member" (singularized from "members")
#[facet(xml::elements, rename = "individual")]
members: Vec<Person>,
}
# let xml_str = r#"<team><individual name="Alice"/><individual name="Bob"/></team>"#;
# let team: Team = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(team.members.len(), 2);
# assert_eq!(team.members[0].name, "Alice");
Collect text nodes with xml::text
<message>Hello <b>world</b>!</message>
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
struct Message {
#[facet(xml::text)]
parts: Vec<String>, // collects text nodes: ["Hello ", "!"]
}
# let xml_str = "<message>Hello world!</message>";
# let msg: Message = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(msg.parts, vec!["Hello world!"]);
Collect attributes with xml::attribute
<element foo="1" bar="2" baz="3"/>
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug)]
#[facet(rename = "element")]
struct Element {
#[facet(xml::attribute)]
values: Vec<String>, // collects all attribute values
}
# let xml_str = r#"<element foo="1" bar="2" baz="3"/>"#;
# let elem: Element = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(elem.values, vec!["1", "2", "3"]);
Flattened Lists (Heterogeneous Children)
When you have a Vec<SomeEnum> and want each enum variant to appear directly as a child element
(without a wrapper), use #[facet(flatten)]:
<canvas>
<circle r="5"/>
<rect width="10" height="20"/>
<path d="M0 0 L10 10"/>
</canvas>
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug, PartialEq)]
#[repr(u8)]
enum Shape {
Circle {
#[facet(xml::attribute)]
r: f64
},
Rect {
#[facet(xml::attribute)]
width: f64,
#[facet(xml::attribute)]
height: f64
},
Path {
#[facet(xml::attribute)]
d: String
},
}
#[derive(Facet, Debug)]
struct Canvas {
#[facet(flatten)]
children: Vec<Shape>, // collects <circle>, <rect>, <path> directly
}
# let xml_str = r#"<canvas><circle r="5"/><rect width="10" height="20"/><path d="M0 0 L10 10"/></canvas>"#;
# let canvas: Canvas = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(canvas.children.len(), 3);
Without #[facet(flatten)], the field would expect wrapper elements:
<!-- Without flatten: expects <child> wrappers -->
<canvas>
<child><circle r="5"/></child>
<child><rect width="10" height="20"/></child>
</canvas>
This pattern is essential for XML formats like SVG, HTML, or any schema where parent elements contain heterogeneous children identified by their element names.
Tuples
Tuples are treated like lists: each element becomes a child element with the field's name (or singularized name for plural field names). Elements are matched by position.
<record>
<value>42</value>
<value>hello</value>
<value>true</value>
</record>
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug, PartialEq)]
#[facet(rename = "record")]
struct Record {
#[facet(rename = "value")]
data: (i32, String, bool),
}
# let xml_str = "<record><value>42</value><value>hello</value><value>true</value></record>";
# let record: Record = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(record.data, (42, "hello".to_string(), true));
Without rename, the field name is used directly (no singularization for tuples since tuple fields typically have singular names):
# use facet::Facet;
# use facet_xml as xml;
#[derive(Facet, Debug, PartialEq)]
#[facet(rename = "record")]
struct Record {
data: (i32, String, bool),
}
# let xml_str = "<record><data>42</data><data>hello</data><data>true</data></record>";
# let record: Record = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(record.data, (42, "hello".to_string(), true));
Enums
In XML, enums are always treated as externally tagged - the element name is the variant discriminator. This is natural for XML because the element structure already provides tagging.
Any #[facet(tag = "...")] or #[facet(content = "...")] attributes are ignored for XML
serialization. These attributes are useful for JSON (which needs explicit tag fields), but
XML doesn't need them since element names serve this purpose.
Unit variants
Unit variants become empty elements:
# use facet::Facet;
#[derive(Facet, Debug, PartialEq)]
#[repr(u8)]
enum Status {
Active,
Inactive,
}
// "Active" becomes <active> (lowerCamelCase)
# let xml_str = "<active/>";
# let status: Status = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(status, Status::Active);
Newtype variants
Newtype variants (single unnamed field) wrap their content:
# use facet::Facet;
#[derive(Facet, Debug, PartialEq)]
#[repr(u8)]
enum Value {
Text(String),
Number(i32),
}
// <text>hello</text> deserializes to Value::Text("hello")
# let xml_str = "<text>hello</text>";
# let value: Value = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(value, Value::Text("hello".into()));
Struct variants
Struct variants have child elements for their fields:
# use facet::Facet;
#[derive(Facet, Debug, PartialEq)]
#[repr(u8)]
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
// <circle><radius>5.0</radius></circle>
# let xml_str = "<circle><radius>5.0</radius></circle>";
# let shape: Shape = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(shape, Shape::Circle { radius: 5.0 });
Variant renaming
Use #[facet(rename = "...")] on variants to override the element name:
# use facet::Facet;
#[derive(Facet, Debug, PartialEq)]
#[repr(u8)]
enum Status {
#[facet(rename = "on")]
Active,
#[facet(rename = "off")]
Inactive,
}
# let xml_str = "<on/>";
# let status: Status = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(status, Status::Active);
Internally/adjacently tagged enums
Attributes like #[facet(tag = "type")] or #[facet(tag = "t", content = "c")] are
ignored for XML. They exist for JSON compatibility but don't affect XML serialization:
# use facet::Facet;
#[derive(Facet, Debug, PartialEq)]
#[repr(u8)]
#[facet(tag = "type")] // ignored for XML!
enum Shape {
Circle { radius: f64 },
}
// Still uses element name as discriminator
# let xml_str = "<circle><radius>3.0</radius></circle>";
# let shape: Shape = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(shape, Shape::Circle { radius: 3.0 });
Untagged enums
Untagged enums (#[facet(untagged)]) use the enum's own name as the element, not a variant name.
The content determines which variant is selected:
# use facet::Facet;
#[derive(Facet, Debug, PartialEq)]
#[repr(u8)]
#[facet(untagged, rename = "point")]
enum Point {
Coords { x: i32, y: i32 },
}
# let xml_str = "<point><x>10</x><y>20</y></point>";
# let point: Point = facet_xml::from_str(xml_str).unwrap();
# assert_eq!(point, Point::Coords { x: 10, y: 20 });
Part of the Facet Ecosystem
This crate is part of the facet ecosystem, providing reflection for Rust.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Dependencies
~8–11MB
~170K SLoC