# 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.
```rust
# 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:
```rust
# 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`).
```xml
<person>
<name>Ella</name>
<age>42</age>
</person>
```
```rust
# 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:
```xml
<link href="/home">Home</link>
```
```rust
# 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:
```xml
<name>Ella</name>
```
```rust
# 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
```xml
<playlist>
<track>Song A</track>
<track>Song B</track>
</playlist>
```
```rust
# 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`
```rust
# 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)
```rust
# 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:
```rust
# 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`
```xml
<message>Hello <b>world</b>!</message>
```
```rust
# 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`
```xml
<element foo="1" bar="2" baz="3"/>
```
```rust
# 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)]`:
```xml
<canvas>
<circle r="5"/>
<rect width="10" height="20"/>
<path d="M0 0 L10 10"/>
</canvas>
```
```rust
# 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:
```xml
<!-- 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.
```xml
<record>
<value>42</value>
<value>hello</value>
<value>true</value>
</record>
```
```rust
# 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):
```rust
# 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:
```rust
# 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:
```rust
# 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:
```rust
# 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:
```rust
# 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:
```rust
# 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:
```rust
# 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 });
```