[!NOTE] The rewrite has landed. Schema parsing, structural validation, simple types with restriction facets, and
xs:patternall work; see Status for exactly what is and is not supported.
0.0.1on crates.io is the old crate, which exposed no public API at all. Do not use it.
Contents
Getting started
Reference
- Why this crate exists — the gap it fills
- The oxml ecosystem — where this fits
- Ecosystem comparison — XSD support in Rust
- Planned capabilities — the roadmap
Practical
Status
| State | |
|---|---|
| Schema parsing | ✅ elements, model groups, cardinality, attributes |
| Simple types | ✅ all 44 built-ins, 12 restriction facets |
xs:list and xs:union |
✅ |
xs:all, xs:group, xs:attributeGroup |
✅ |
xs:any and xs:anyAttribute |
✅ namespace and processContents |
| Complex-type derivation | ✅ extension, restriction, and Particle Valid (Restriction) |
| Schema validity | ✅ XSD's own structural rules |
xs:pattern |
✅ own engine, XSD dialect including class subtraction |
| Diagnostics | ✅ every violation, each with a path |
| Conformance | ✅ 95.6% of the W3C suite's decided tests |
| Tests | ✅ 207 |
Identity constraints (key, keyref, unique) |
✗ |
xs:import / include |
✗ |
| Substitution groups | ✗ |
An unsupported construct is skipped rather than rejected: the surrounding rules still apply, so a schema using one validates everything else correctly instead of failing wholesale.
What was skipped is reported. support::unsupported audits a
schema against what this crate enforces and names everything it does
not, so "this document is valid" and "this document was checked" are
never confused for one another.
Install
[]
= { = "https://github.com/sebastienrousseau/xmlschema" }
= { = "https://github.com/sebastienrousseau/oxml" }
Published releases follow once the suite cuts its first version together.
Quick Start
use ;
let xsd = r#"
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="book">
<xs:complexType>
<xs:sequence>
<xs:element name="title" type="xs:string"/>
</xs:sequence>
<xs:attribute name="lang" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>
"#;
let schema = parse_schema?;
let doc = parse?;
assert!;
# Ok::
Every violation is reported, each with a path:
/invoice/issued — `22/08/2026` is not a valid date (YYYY-MM-DD)
/invoice/line[1]/@currency — `pounds` does not match the pattern `[A-Z]{3}`
/invoice/line[1]/amount — -5 must be greater than 0
/invoice/line[2] — missing required attribute `currency`
/invoice/line[2]/amount — `not a number` is not a valid decimal
Why this crate exists
Rust has no pure-Rust XSD validator. The options today are:
libxml— bindings to libxml2. Complete and battle-tested, but it is C: it needs a build toolchain, containsunsafe, does not work in WebAssembly, and inherits libxml2's CVE stream.- Nothing else. There is no maintained pure-Rust implementation.
For a project already committed to safe Rust — no C toolchain, WASM targets, an auditable dependency tree — that is not a choice so much as an absence.
xmlschema exists to close it, with the same constraints as the rest
of the suite: #![forbid(unsafe_code)], no FFI, no C.
The oxml ecosystem
Every member ships the same version number, so there is never a compatibility table to consult.
| Crate | What it is | Status |
|---|---|---|
oxml |
Core — parser, tree, XPath 1.0 | Available |
oxml-cli |
Command-line querying and formatting | Planned |
oxml-lsp |
Language server | Planned |
oxml-mcp |
Model Context Protocol server | Planned |
oxml-wasm |
WebAssembly bindings | Planned |
xmlschema |
XSD validation | Being rewritten |
This crate keeps its published name rather than being folded into
oxml. The name means XSD validation specifically, and repurposing it
into a general toolkit would have handed existing users something
entirely different under a name they already depend on.
Ecosystem comparison
| Crate | XSD validation | Pure Rust | WASM | Last release |
|---|---|---|---|---|
xmlschema |
planned | ✅ | ✅ | 2023 (unusable) |
libxml |
✅ | ✗ (C-FFI) | ✗ | active |
quick-xml |
✗ | ✅ | ✅ | active |
roxmltree |
✗ | ✅ | ✅ | active |
xot |
✗ | ✅ | ✅ | 2025 |
Planned capabilities
In order:
- Schema parsing — read an
.xsdinto a usable model, built onoxml's tree. - Structural validation — elements, attributes, cardinality, sequence/choice/all.
- Simple type validation — the built-in datatypes, restrictions, patterns, enumerations.
- Complex types — extension, restriction, mixed content.
- Diagnostics — every violation reported with an element path and a reason, so a caller can fix all of them in one pass rather than probing one failure at a time.
Import mechanisms (xs:import, xs:include, xs:redefine) come after
the core is correct, because they multiply the surface without adding
validation power.
Examples
examples/ is compiled and run in CI.
| Example | What it shows |
|---|---|
validate |
Parsing a schema once, validating many, and reading a Report |
Reading a report
validate returns a Report, not a Result. A document can be wrong
in several independent ways, and stopping at the first means fixing
them one build at a time.
/order: missing required attribute `id`
/order: expected `customer` exactly once, found 0
/order/line[1]: expected `sku` exactly once, found 0
/order/line[1]/qty: `many` is not a valid integer
/order/line[1]/sku: unexpected element `sku`; this content model allows sku, qty in that order
Each Violation carries a path and a message. The path is
positional — line[1] is the first line child — so it identifies one
element rather than a set.
Migration
From xmllint --schema
xmllint |
xmlschema |
|---|---|
xmllint --schema s.xsd --noout f.xml |
validate(&parse(xml)?, &parse_schema(xsd)?) |
| exit status | report.is_valid() |
| stderr text | report.violations, each with a path |
--schema with xs:import |
not supported yet |
The useful difference is that violations are data rather than a stream of text to grep.
From libxml's XmlSchemaValidationContext
libxml |
xmlschema |
|---|---|
SchemaParserContext::from_buffer |
parse_schema |
SchemaValidationContext::validate_document |
validate |
| error callbacks | report.violations |
| a libxml2 C dependency | none |
libxml2 implements XSD 1.0 completely and this crate does not — see
Status. If you need xs:import, identity constraints or
complex-type derivation today, stay.
When not to use xmlschema
- You need complete XSD 1.0. This is early; check Status against your schemas first.
- You need XSD 1.1 — assertions, conditional type assignment. Xerces has it.
- Your schemas use
xs:importorxs:include. Not supported; those constructs are skipped, so validation is incomplete rather than wrong. - You need identity constraints —
xs:key,xs:keyref,xs:unique. - You need to validate while streaming. The document is parsed in full first.
FAQ
Why does an unsupported construct get skipped rather than rejected?
Because a schema using one construct this crate lacks would otherwise
be unusable in full. Skipping means the surrounding rules still apply,
so a schema with an xs:all block validates everything else
correctly.
The cost is that a document can be reported valid when a construct that was skipped would have rejected it. Validation is incomplete, not wrong — and the distinction matters, so check Status before relying on a pass.
Why is xs:pattern a hand-written engine?
Because XSD's regular expression dialect is not PCRE and not Rust's
regex. It has different anchoring semantics — the whole value must
match — its own character-class escapes, and Unicode block and category
escapes that neither crate spells the same way.
Using a general-purpose engine would mean translating one dialect into another and being subtly wrong at the edges. The engine is a few hundred lines and does exactly what the specification says.
Is a schema reusable across documents?
Yes, and that is the intended shape. parse_schema is the expensive
half; validate is the half you repeat. A Schema is immutable after
parsing.
Does it fetch schemas over the network?
No. parse_schema takes the schema's text. There is no code that
opens a file or a socket, which is also why xs:import and
xs:include are not supported — they name a location to fetch.
When they arrive, the shape will be a caller-supplied map from location to content, never a fetch.
What does a path like /order/line[1]/qty mean?
The qty child of the first line child of order. It is positional
so that it identifies one element and not a set — which is what you
need when the message is "this one is wrong".
Does it validate the schema itself?
Partly. It rejects a schema that is not well-formed XML, reports what
it cannot understand, and enforces the structural rules XSD imposes on
schemas themselves — where xs:annotation may appear, which children
are mutually exclusive, that two element declarations of one name must
agree on their type, that a facet's value must belong to the type it
narrows, and that a restriction's content model must be a valid
restriction of its base.
It does not validate a schema against the full XSD schema-for-schemas, which would be a second validator.
How is this tested?
207 tests over schema parsing, every built-in type, every facet, the pattern engine, the validator and the derivation relation. The XML underneath carries the W3C XML conformance suite — 2,520 of 2,557 decided tests, zero panics.
And the W3C XML Schema Test Suite, xsts-2007-06-20, pinned by
SHA-256: 39,420 tests, of which 95.6% of the decided ones pass,
with zero panics. This was the main gap in the crate's verification
until 0.0.6, and closing it is most of what 0.0.6 is.
A pass rate over that suite is only worth reporting because of how it
is counted. This crate implements a subset of XSD and skips what it
does not understand — and a schema whose constraints were all skipped
accepts every document, so agreeing with a test proves nothing. A
test counts as a pass only when the schema is enforced in full. On
the first run, 20,682 tests would otherwise have counted as passes
with nothing checked; that figure is published alongside the rate, in
doc/CONFORMANCE.md.
Development
Security
XSD validation is normally applied to untrusted documents, which makes
the parser's threat model part of this crate's threat model. It
inherits oxml's posture:
- No entity expansion. Only the five predefined entities and numeric character references are resolved, so XXE and billion-laughs are foreclosed by construction rather than by a flag.
- No
unsafe.#![forbid(unsafe_code)], enforced at compile time.
Report vulnerabilities privately — see SECURITY.md.
Documentation
Acknowledgements
- libxml2 — the reference implementation, and the yardstick for behaviour.
- W3C — for the XML Schema specification.
- python-xmlschema — proof that a readable, standalone XSD implementation is achievable.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.