Leer y escribir XML en Rust

En Rust el XML se maneja con quick-xml, que ofrece dos caminos: recorrer el documento etiqueta por etiqueta, o declarar una estructura y dejar que serde la rellene. Esta página usa el segundo, que es el que resuelve la mayoría de los casos con menos código.

La dependencia

cargo add quick-xml --features serialize
cargo add serde --features derive
[dependencies]
quick-xml = { version = "0.42.0", features = ["serialize"] }
serde = { version = "1.0.229", features = ["derive"] }

De XML a structs y de vuelta

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename = "inventario")]
struct Inventario {
    #[serde(rename = "articulo")]
    articulos: Vec<Articulo>,
}

#[derive(Debug, Deserialize, Serialize)]
struct Articulo {
    #[serde(rename = "@id")]
    id: u32,
    nombre: String,
    unidades: u32,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let xml = r#"<inventario>
  <articulo id="1"><nombre>tornillos</nombre><unidades>120</unidades></articulo>
  <articulo id="2"><nombre>tuercas</nombre><unidades>80</unidades></articulo>
</inventario>"#;

    let inv: Inventario = quick_xml::de::from_str(xml)?;
    for a in &inv.articulos {
        println!("{} | {:<10} | {} unidades", a.id, a.nombre, a.unidades);
    }

    let salida = quick_xml::se::to_string(&inv)?;
    println!("\nde vuelta a XML:\n{salida}");
    Ok(())
}
salida1 | tornillos  | 120 unidades
2 | tuercas    | 80 unidades

de vuelta a XML:
<inventario><articulo id="1"><nombre>tornillos</nombre><unidades>120</unidades></articulo><articulo id="2"><nombre>tuercas</nombre><unidades>80</unidades></articulo></inventario>

La diferencia con JSON está en los nombres. XML distingue entre atributos y elementos, y serde necesita saber cuál es cuál: el prefijo @ marca un atributo, como en #[serde(rename = "@id")]. Sin esa arroba, serde buscaría un elemento <id> y no lo encontraría.

Y el nombre del elemento raíz. Sin #[serde(rename = "inventario")] sobre la estructura, el XML generado sale como <Inventario> con mayúscula, porque serde usa el nombre del tipo de Rust, que por convención va en mayúscula inicial. Para leer da igual, pero al escribir produce un documento que no coincide con el original. Conviene ponerlo siempre.

Correspondencias entre XML y Rust

En el XML En la estructura
Un atributo #[serde(rename = "@id")]
Un elemento hijo un campo con el mismo nombre
Varios hijos iguales un campo Vec<T>
Un hijo que puede faltar un campo Option<T>
El texto dentro de la etiqueta #[serde(rename = "$text")]
Cambiar el nombre de la raíz #[serde(rename = "...")] en el struct
Cuándo no usar serde para XML. Este camino va bien con documentos de forma conocida y regular. Si el XML es enorme y no cabe en memoria, o la estructura es irregular y llena de excepciones, conviene el lector de eventos de quick-xml, que recorre el documento sin construirlo entero. Ten en cuenta que ese API cambia entre versiones con cierta frecuencia: en la 0.42 varios métodos que antes devolvían bytes devuelven ya cadenas.

El código de esta página se compiló y ejecutó con rustc 1.96.1 y quick-xml 0.42.0 antes de publicarla; las salidas están copiadas de la ejecución real. Documentación oficial: quick-xml en docs.rs.