facet-xml 0.43.1

XML serialization for facet using the new format architecture - successor to facet-xml
Documentation

facet-xml

Coverage Status crates.io documentation MIT/Apache-2.0 licensed Discord

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 });

Sponsors

Thanks to all individual sponsors:

...along with corporate sponsors:

...without whom this work could not exist.

Special thanks

The facet logo was drawn by Misiasart.

License

Licensed under either of:

at your option.