quick-xml: a pull XML parser that borrows instead of copying
Rust high performance xml reader and writer
At a glance
- What is it?
- quick-xml is a Rust XML pull reader and writer whose events borrow from the input wherever it can, with Cow for the cases where it cannot, a buffer you can reuse, optional serde support, and a plain event API for when you do not want serde at all. It is MIT licensed and its current line is 0.42.
- Who is it for?
- Adopt quick-xml if you are parsing XML in Rust and want control over allocation, whether you use its serde support or its event API, and you are willing to read the borrow rules carefully. Do not adopt it if you want a DOM, because a pull parser gives you events rather than a tree, and it will not build you a document.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Borrowing by default, Cow for the rest
quick-xml is described as a high performance XML pull reader and writer, and the design decisions in the reader are what make that claim credible. The reader is almost zero-copy, using Cow whenever possible, and it is easy on memory allocation, with the API providing a way to reuse buffers. That is the core of the library. Most XML parsers in most languages build a tree of owned strings, and the copying is where the time goes. A pull parser that hands you events instead of a tree has a different shape of cost, and quick-xml pushes that as far as it can: names and text come out as &str rather than as bytes, with the note in the example that quick-xml validates that the input is UTF-8. When the parser does need to produce owned data, it hands back a Cow so the caller decides whether to keep the borrow or take ownership. The syntax is inspired by xml-rs, which is a useful detail for anyone porting code across parsers.
The event loop, and the buffer discipline it expects
The reader example is worth reading as documentation of the intended usage, because the API asks something of the caller. You create a Reader, optionally configure it, and loop over read_event_into with a buffer you own. The events are Start, Text, End, Eof and others, and you match on them. The important convention is at the end of the loop, where the buffer is cleared if you are not keeping a borrow elsewhere, with the comment that this keeps memory usage low. That is the deal the library offers: you manage one buffer, you clear it when you are done with the borrowed data, and the allocation cost of parsing collapses. The reader also exposes trim_text through config_mut, and the text event requires an explicit choice about entity handling, with xml_content unescaping entities and the XmlVersion selecting the normalisation rules between 1.0 and 1.1, where most documents are 1.0. Support for various encodings comes behind the encoding feature, and namespace resolution and special character handling are part of the reader rather than something bolted on.
Writing is the same event stream, inverted
The writer mirrors the reader, and the example shows the shape. You construct a Writer over a Cursor, then feed it events. The interesting case is transforming a document as you copy it, where the example takes a Start event for this_tag, builds a new BytesStart element called my_elem, extends it with the attributes collected from the original, pushes a new attribute, and writes the new Start event, then writes the matching End event with BytesEnd::new("my_elem"), and copies every other event through unchanged. The expected result is asserted at the end, which is the sort of test that makes the API pleasant to use. The point of the design is that a Writer accepts the same Event type the Reader produces, so a read-transform-write pipeline is a single loop with no intermediate representation, and the library does not force you to materialise a document to move data around inside one.
Serde support and the $text versus $value question
With the serialize feature enabled, quick-xml works with serde's Serialize and Deserialize traits, and the documentation for deserialization describes in detail the mapping between XML and Rust types, in particular the syntax that lets you distinguish elements from attributes. The design is credited to serde-xml-rs, and quick-xml follows that project's convention for deserialization, including the special name for parsing the value of a tag. The distinction the documentation walks through is the one that trips people up. If you have an input of the form <foo abc="xyz">bar</foo> and want the bar, you can use either the special name $text or the special name $value, and the difference is written up in the deserialization documentation:
struct Foo {
#[serde(rename = "@abc")]
pub abc: String,
#[serde(rename = "$text")]
pub body: String,
}The @ prefix in the attribute rename is the other half of the convention, and together the two make the mapping from XML to a Rust struct explicit rather than guessed. If you do not want serde, the event API is the alternative, and the examples directory carries a guide to which of the several approaches to reach for.
The performance numbers, and what they are worth
The project publishes benchmarks, and it is careful to qualify them. It notes that benchmarking is hard and that results depend on your input file and your machine, and the numbers it gives are from a particular file. On that file, quick-xml is around 50 times faster than the xml-rs crate, with bench_quick_xml at 198,866 ns per iteration against 14,468,930 ns for xml_rs, and there are separate benches for escaped and namespaced input, plus the serde comparison where bench_serde_quick_xml at 1,181,198 ns sits against bench_serde_xml_rs at 15,039,564 ns. The authors also point out that despite not focusing on performance, with several unnecessary copies in the implementation, it remains about 10x faster than serde-xml-rs. The honest reading is that the pull-parser design has a real advantage over a DOM-based parser on large documents, and that the numbers are indicative of an order of magnitude rather than a guarantee, since the library itself says the result depends on your input and your machine. The README also points at a third party parser comparison table for a broader view.
Features, packaging and a tight dependency set
The crate has two optional features. encoding adds support for non-UTF-8 XML, and serialize adds the serde support described above. The default dependency set is small and deliberate: memchr for byte searching, serde when the feature is on, encoding_rs for the encoding feature, and tokio with the io-util feature for asynchronous readers, plus arbitrary for fuzzing. The manifest sets edition 2024 with a rust-version of 1.86, and the package deliberately excludes tests, examples and benches to reduce the published size, with a comment that this is the source of packaging warnings in recent cargo. The repository layout supports the project's testing story, with src/, benches/, a compare/ directory for parser comparisons, a fuzz/ directory, a test-gen/ directory, and a tests/ directory, alongside AGENTS.md and CLAUDE.md for automated contributors, a .rustfmt.toml, and a Changelog.md.
Where quick-xml is the wrong tool
Four limits are clear. The first is that a pull parser is not a DOM. If your code wants to ask a question about the document as a whole, such as find the element with attribute x anywhere in the file, a pull parser makes you either buffer the events yourself or run the search incrementally with state you maintain, and that is a real amount of code. The second is the borrow discipline, which is a genuine ergonomic tax. The events borrow from the input, the buffer has to be cleared at the right moment, and the Cow in the type signature is the library telling you the borrow might have to become an allocation. Code that is careless with lifetimes will be a fight, and the examples exist precisely because the pattern is not obvious. The third is scope. There is no schema validation, no XPath, no XSLT, no DTD handling and no entity expansion defence worth the name, so if you are consuming XML from an untrusted source, the security question lands on your code rather than on the library. The fourth is the maintenance signal. The current release is v0.42.0 from 2026-08-22, the 0.41.0 release from 2026-06-29 carried security fixes, and 0.40.1 from 2026-05-15 fixed a rarely possible serde deserialization panic, which is a useful reminder that a parser library carries a security maintenance obligation of its own.
Editorial conclusion
Adopt quick-xml if you are parsing XML in Rust and want control over allocation, whether you use its serde support or its event API, and you are willing to read the borrow rules carefully. Do not adopt it if you want a DOM, because a pull parser gives you events rather than a tree, and it will not build you a document. Before you upgrade, note the 0.42 line and read the changelog, because the current release is v0.42.0 from 2026-08-22, the previous one fixed a rarely possible serde deserialization panic, and the one before that carried security fixes.
Frequently asked questions
What is quick-xml?
It is a high performance XML pull reader and writer written in Rust, MIT licensed, whose syntax is inspired by xml-rs. The reader is almost zero-copy using Cow whenever possible, and provides a way to reuse buffers to keep memory allocation low.
Does quick-xml support serde?
Yes, behind the serialize feature, using serde's Serialize and Deserialize traits. The mapping between XML and Rust types, including how elements are distinguished from attributes, is described in the deserialization documentation, and the convention follows serde-xml-rs including the $value special name.
What is the difference between $text and $value in quick-xml?
For an input like <foo abc="xyz">bar</foo> you can reach the bar through either special name, and the difference is explained in the deserialization documentation under the difference between text and value special names. The $text form is shown in the README example alongside the @ prefix used to rename an attribute.
How does quick-xml compare to xml-rs on performance?
The project states that on a particular file quick-xml is around 50 times faster than the xml-rs crate, with bench_quick_xml at 198,866 ns per iteration against 14,468,930 ns for xml_rs, while noting that benchmarking is hard and results depend on your input file and your machine.
Which features does the quick-xml crate have?
Two optional features: encoding for non UTF-8 XML, and serialize for serde support. The crate is at version 0.42.0 with edition 2024 and a rust-version of 1.86, and the published package excludes tests, examples and benches to keep it small.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tafia-quick-xml)Was this useful?