[!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
- Status — what works today, honestly
- Install — Cargo
- Quick Start — validate a document in ten lines
The oxml ecosystem
- The oxml ecosystem — seven crates, one version
Reference
- Why this crate exists — the gap it fills
- Ecosystem comparison — XSD support in Rust
- Reading a report — every violation, each with a path
- Migration — from
libxml - What is not implemented — and what comes next
- Benchmarks — schema parsing, validation, the pattern engine
Practical
- Examples — runnable, and run in CI
- When not to use xmlschema
- FAQ
- Development
- Security
- Roadmap
- Documentation
- Acknowledgements
- License
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.2% of the W3C suite's decided tests, ratcheted |
| Tests | ✅ 241, plus the conformance suite |
Identity constraints (key, keyref, unique) |
✅ scoped selectors and fields |
xs:import / include |
✅ with caller-supplied SchemaSource |
| 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
[]
= "=0.0.11"
= "=0.0.11"
Use matching family versions so documents crossing the core/schema boundary have one Rust type. This is the 0.0.11 release candidate; registry availability is verified separately before publication.
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 | Available |
oxml-lsp |
XML analysis and linting; the LSP transport is not yet implemented | Available |
oxml-mcp |
Model Context Protocol server | Available |
oxml-wasm |
WebAssembly bindings | Available |
xmlschema |
XSD validation | Available, with the limits above |
oxml-json |
JSON support for the language server |
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 | Status |
|---|---|---|---|---|
xmlschema |
✅ 95.2% of the W3C suite's decided tests | ✅ | ✅ | active |
libxml |
✅ | ✗ (C-FFI) | ✗ | active |
quick-xml |
✗ | ✅ | ✅ | active |
roxmltree |
✗ | ✅ | ✅ | active |
xot |
✗ | ✅ | ✅ | active |
libxml remains the more complete implementation. The difference is
what it costs: a C toolchain, unsafe, no WebAssembly target, and
libxml2's CVE stream. This crate trades completeness for those.
What is not implemented
Everything in the list this section used to hold has shipped — schema parsing, structural validation, simple types, complex types and diagnostics are all ✅ in Status above. What remains:
Identity constraints and caller-supplied imports/includes are
implemented. Remaining gaps include xs:redefine, substitution groups
and unsupported corners of derivation validity. Check support::unsupported
for the actual schema, rather than assuming a validation pass is complete.
An unsupported construct is skipped rather than rejected, and the conformance harness counts such a test as unsupported whatever answer it produced — so the published rate never flatters itself with accidental agreement.
Benchmarks
Three benchmarks, split because they answer different questions. Parsing a schema is the expensive half and happens once; validating is the cheap half and repeats. The pattern engine is separate because it is a regex implementation of its own, in XSD's dialect rather than PCRE's.
No absolute figures are published here. The same benchmarks on this machine returned confidence intervals spanning 566–906 µs for a single case — a spread wider than most changes worth measuring — because the machine was busy. A figure without its conditions is not a measurement. See oxml's BENCHMARKS.md for the method and what a published number has to carry.
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 constructs reported as unenforceable by
support::unsupported, use a validator that implements those constructs.
Examples
examples/ is compiled and run in CI.
| Example | What it shows |
|---|---|
validate |
Parsing a schema once, validating many, and reading a Report |
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.
- You need automatic schema fetching. Imports and includes require
parse_schema_withand a caller-suppliedSchemaSource; no network or filesystem resolver is enabled by default. - 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. parse_schema_with resolves imports and
includes through the SchemaSource supplied by the caller; parse_schema
uses NoSchemas and reports unresolved constructs as unenforceable.
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?
241 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,557 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.2% 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
That runs everything CI runs, in the order that fails fastest: format,
clippy, tests, rustdoc, the #![forbid(unsafe_code)] check, the
example, the W3C XSD conformance suite, the 95% coverage floor and an
MSRV build. It pins the toolchain rather than trusting
rust-toolchain.toml, because a RUSTUP_TOOLCHAIN in the environment
silently overrides that file and a lint that exists in one release and
not another then makes a green local run and a red CI one.
The conformance step is skipped loudly when the suite has not been
downloaded, and counts as a failure rather than vanishing. A skipped
conformance test is a passing one as far as cargo test is concerned,
and this crate's headline figure rests on that suite.
The individual steps, if you want them one at a time:
CI runs the same set on Linux, macOS and Windows.
Security
Scorecard scans use a digest-pinned upstream image. Results are retained as CI artifacts and repository posture findings in GitHub code scanning, not published to Scorecard's public service. The badge shows workflow status, not a security score; a successful scan can still report findings.
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 automatic external retrieval. The core never fetches external entities. Internal entity expansion is supported within its configured budgets; caller-supplied schema resolution remains the caller's trust boundary.
- 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.