quick-xml examples
August 22, 2026 · View on GitHub
There are many ways to use quick-xml, this guide is intended to help you choose which of its APIs to use. There are two, from highest-level to lowest:
- serde (the
serializefeature) — map XML directly onto#[derive(Serialize, Deserialize)]structs. Best when your XML corresponds cleanly to Rust types. - the pull
Readerand pushWriter— work with a stream ofEvents (Start,Text,End, ...). Best when you need streaming, partial parsing, or to transform a document. TheWriteradditionally offers a high-level builder and a low-level event API; see Writing below.
For simple jobs, it's best to start with serde and drop to the Reader/Writer
only when it doesn't fit. For parsing especially large or complex documents however,
using the lower-level APIs is likely to be a better approach.
Which approach should I use?
Reading
The rule of thumb, in order of preference:
-
serde — if the document is reasonably sized, performance is not critical, and the shape maps onto structs, this is the least code by far. You describe the types once and get parsing for free. See
serde_roundtrip.rs -
A state machine — when the items you want are simple and not too deeply nested, a hand-written
Readerloop that tracks its position in an explicitenumis fast, allocation-light, and makes the grammar you accept obvious. Matching on(state, event)pairs shows exactly what you expect where, and a single read buffer serves the whole document. Seereader_patterns.rs -
Nested readers / split functions — when the document has many levels, handing each subtree to its own function reads more naturally than one giant state enum. The trade-off is that it is harder to reuse a single read buffer across levels, so deeply nested parsing can allocate more. See
reader_patterns.rs,nested_readers.rs
In practice case 1 is far more common than 2, which is far more common than 3. Reach for the lower-level options mainly when serde can't express the mapping, when you are streaming data too large to hold in memory, or when parsing is hot enough that avoiding the intermediate structs matters.
If more than one part of your program needs the same document, or if you want the
parsing logic to live apart from what consumes it — layer a visitor over any
of the above: one driver walks the document and calls back into a trait, and
each consumer implements only the callbacks it needs. This is a powerful design
pattern that can greatly simplify code re-use in the future. See visitor.rs
Writing
-
serde — if you already have the data in Rust structs, serializing them directly is the least effort. See
serde_roundtrip.rs -
The high-level
Writer::create_elementbuilder — an extension of the standardWriterAPI which handles writing the opening and closing element tags and makes adding attributes more natural. A closure is provided for writing the inner content of the element. Seewriter.rs -
Low-level
Writer::write_event— emit eachStart/Text/Endevent yourself. Most verbose and easiest to unbalance, but gives total control and is ideal when transforming a document (read an event, tweak it, write it back out). Seeswriter.rs, and the transform example in the crate's README
Buffered vs. borrowed reading
Reader::from_str/Reader::from_readerover a&[u8]can return events that borrow from the input, so you callread_event()with no buffer.- Streaming sources (files, sockets) use
read_event_into(&mut buf)and write into aVec<u8>you supply. Reusing (andclear()-ing) that one buffer across the loop keeps allocations low. Seeread_buffered.rs
The examples
Start here
getting_started.rs— the canonical pull-reader loop: create a reader, match events, pull out attributes and text. Read this first.
Reading
reader_patterns.rs— state machine vs. nested readers, side by side, parsing the same document into the same result.visitor.rs— the visitor pattern: one parser driver, a trait of callbacks, and several consumers that each override only what they need.read_buffered.rs— streaming with a reusable buffer.nested_readers.rs— walking several levels to extract data from a real-world (ECMA-376) document.read_nodes.rs— dispatching on top-level nodes by hand.custom_entities.rs— resolving custom&entity;definitions.read_utf16.rs— non-UTF-8 input viaDecodingReader(needs--features encoding).
Writing
writer.rs— the high-level builder and the low-level event API, producing identical output.
serde
serde_roundtrip.rs— mapping XML to structs and back: attributes (@), text ($text), nesting, sequences, and type conversion. Read this to learn the serde field-naming rules (needs--features serialize).read_nodes_serde.rs— the serde counterpart toread_nodes.rs(needs--features serialize).flattened_enum.rs— choosing an enum variant from an attribute with a custom (de)serializer (needs--features serialize).
Running an example
cargo run --example getting_started
cargo run --example serde_roundtrip --features serialize
cargo run --example read_utf16 --features encoding