xsdkit
A generic XSD reader: parse W3C XML Schema into a queryable schema component model, in Rust and Python.
Documentation — guide, Python API reference and the full rustdoc.
Status: early. The component model, document loading, reference resolution, content-model compilation, instance validation and the Python bindings work, and are measured against the W3C XML Schema Test Suite on every change. XSD 1.1 assertions and conditional type assignment come next.
Why
XSD is three languages: schema documents, schema components, and
validation semantics defined over those components. The specification
defines every rule against the middle layer — and in Rust, nothing exposes
it. xsd-parser builds a codegen-shaped intermediate that discards
validation semantics; uppsala builds a model internally but exposes only
validate(). Neither can answer "what are the possible children of this
element, and can they repeat?"
xsdkit builds that layer and hands it to you. Python is no better served:
xmlschema is complete and the only real option, and by its own benchmarks
runs 40–75× slower than lxml.
It is deliberately a reader, not a toolchain. Code generation is
xsd-parser's job, and generating a
config or a binding for some particular downstream reader belongs in a library
of its own — so that reading a schema never pulls in dependencies you did not
ask for.
Usage
[dependencies]
xsdkit = "0.2"
use xsdkit::SchemaSetBuilder;
let schemas = SchemaSetBuilder::new()
.search_path("schemas/")
.file("report.xsd")
.compile()
.into_result()?;
let report = schemas.element(Some("urn:example"), "report").unwrap();
// Which children may appear, may they repeat, may they be absent?
// Answered from the compiled automaton, with substitution groups expanded
// and inherited content included — all three from one pass over the model.
for child in report.children() {
println!(
"{} repeating={} optional={}",
child.display_name(),
child.repeats(),
child.optional(),
);
}
// Attributes, and the type of each.
for a in report.attributes() {
println!("@{} {}", a.local_name(), a.type_of().display_name());
}
// Does a sequence of children satisfy the content model?
let ty = report.type_of().id();
let mut m = schemas.match_content(ty).unwrap();
let ok = m.step(schemas.qname(Some("urn:example"), "title").unwrap()) && m.accepts_end();
Names resolve to references — ElementRef, TypeRef, ChildRef — each a
borrow of the schema plus an id, so following a schema costs no allocation
and no reference counting. Components still live in arenas addressed by
Copy id: element_id and its siblings hand those back directly, every
reference exposes .id(), and schemas.get(id) goes the other way.
Ask about a whole type's children at once rather than child by child. Each
of the singular predicates walks the content model, so a type with hundreds
of children — ordinary in GML, UBL or WITSML — pays for hundreds of walks;
children() does the same work in one, and is about 40× faster on those
schemas.
Caching a compiled schema
Compiling is the expensive step. The serde feature makes Schemas — and
every component it holds — serializable, so a large schema set is compiled
once and loaded thereafter:
xsdkit = { version = "0.2", features = ["serde"] }
fn cache(schemas: &xsdkit::Schemas) -> Result<xsdkit::Schemas, postcard::Error> {
let bytes = postcard::to_allocvec(schemas)?;
postcard::from_bytes(&bytes)
}
Any serde format works, self-describing ones included. On a 900 KB schema of 2,000 types this is a 7x speedup — 31 ms to compile against 4.5 ms to load — at the cost of a cache several times the size of the source XSD. Measure it on your own schema before deciding:
cargo run --release --features serde --example cache -- main.xsd [search/path ...]
The format is not stable across versions of xsdkit: names are interned and every component refers to them by index, so a cache is only meaningful alongside the code that wrote it. Key it on the crate version and rebuild on a miss.
Python
pip install xsdkit
import xsdkit
schemas = xsdkit.SchemaSet.from_file("report.xsd", search_paths=["schemas/"])
report = schemas["{urn:example}report"]
report.tree() # or print(...); it renders in a notebook either way
# in Jupyter, a collapsible colour-coded tree
# report
# title: xs:string
# item+
# @sku
# price: xs:decimal
# note?: xs:string
report["item"]["price"].type.qname # walk by name, no `.type` hop
[child.local_name for child in report]
report["item"].repeats # occurrence belongs to the pair,
report["item"]["note"].optional # and a child carries its own
len(schemas) # globals this schema declares
"{urn:example}report" in schemas # a mapping: dict(schemas) works too
# Does a child sequence satisfy the content model?
report.type.accepts(["{urn:example}title", "{urn:example}count"])
Validate a document, and read it into typed values:
report = schemas.validate(open("report.xml").read())
report.is_valid # False
for d in report.errors:
print(d) # error[XSD2004]: `{urn:example}count`: ... --> :3
for ev in schemas.iter_typed(open("report.xml").read()):
if ev.kind == "text":
print(type(ev.value).__name__, ev.value)
# int 42
# Decimal 3.14
# datetime 2024-12-30 12:39:15+00:00
# date 2024-03-31
Values arrive as native Python types, not strings to re-parse. iter_typed
composes with enumerate, itertools and generator expressions, and carries
the outcome on its .report — before the loop as well as after.
XSD 1.1 is opt-in, as it is in Rust, and documents may be bytes whose encoding is detected rather than assumed:
from pathlib import Path
schemas = xsdkit.SchemaSet.from_file("report.xsd", version="1.1")
schemas.validate(Path("report.xml").read_bytes())
Schemas need not be on disk. A resolver is a function of (location, base)
that returns the document, or raises to say it could not be found:
with zipfile.ZipFile("schemas.zip") as z:
schemas = xsdkit.SchemaSet.from_string(main, resolver=lambda loc, _: z.read(loc))
Schemas that are expected to be imperfect return their diagnostics instead of raising:
schemas, diagnostics = xsdkit.load("vendor/partial.xsd", conformance="lax")
for d in diagnostics:
print(d) # error[XSD1201]: ... --> file.xsd:12
Inspect a schema from the command line:
cargo run --example inspect -- schemas/report.xsd --lax
What works today
- The component model — types, elements, attributes, particles, model groups, wildcards, identity constraints, notations, annotations; all seven symbol spaces kept separate.
- All 50 built-ins as real components, so
xs:stringresolves exactly like a user type. 19 primitives, the 1.1 additions, and the derivation chains between them. - Facets with correct composition: patterns OR within a restriction step
and AND across steps; the innermost enumeration wins;
whiteSpaceapplied before lexical parsing. - Composition —
include,import,redefineandoverride, including chameleon includes, where a document with notargetNamespaceis absorbed into its includer's. Circular graphs terminate. - Resolution — references, attribute-group flattening (transitive),
substitution-group closure (transitive, skipping abstract heads),
keyref→key. - Instance validation in one streaming pass over
quick-xml, with a typed PSVI: values arrive asValue::Integer(42), not"42". Handlesxsi:type— prefix, derivation,blockand abstractness —xsi:nil, substitution groups and wildcards. - Identity constraints —
xs:key,xs:keyrefandxs:uniqueover their XPath subset, with keys compared in the value space — andxs:ID/xs:IDREFuniqueness and resolution, enforced in that same pass. - Content models compiled to Glushkov position automata, with
Unique Particle Attribution checking falling out of the same structure.
Extension appends to the base's content; restriction replaces it.
xs:allgets per-member counters rather thann!regex paths. - XSD 1.1, opt-in via
Version::Xsd11:openContent,defaultOpenContent,defaultAttributes,vc:conditional inclusion,notNamespace/notQName,xs:precisionDecimal, the relaxedxs:allrules, and the relaxed UPA rule where an element particle beats a competing wildcard. - Diagnostics with stable codes, source spans and help text. Every error is reported, not just the first.
Roadmap
| ✅ | Component model, loading, composition | done |
| ✅ | Content automata, UPA | done |
| ✅ | Python bindings, type stubs, encoding detection | done |
| ✅ | Instance validation, typed reading (PSVI) | done |
| ✅ | redefine / override |
done |
| ✅ | XSD 1.1 open content, default attributes, relaxed UPA | done |
| ✅ | Identity constraints, xs:ID / xs:IDREF |
done |
| → | XSD 1.1 assertions and conditional type assignment | next |
Code generation is permanently out of scope.
Encodings
Document encodings are detected from a byte-order mark, then the XML declaration, then UTF-8; bytes that contradict the encoding they claim are an error rather than a document quietly full of replacement characters.
Diagnostics
Building returns every diagnostic at once:
error[XSD1201]: no type named `{urn:example}Missing`
--> schemas/report.xsd:14
help: check the spelling, or add an xs:import for its namespace
Conformance::Lax downgrades violations that still permit building
components — real schemas ship with dangling imports often enough that the
mode earns its keep.
use xsdkit::{Compilation, Conformance, SchemaSetBuilder};
let Compilation { schemas, diagnostics } = SchemaSetBuilder::new()
.conformance(Conformance::Lax)
.file("vendor/partial.xsd")
.compile();
Conformance
Measured against the W3C XML Schema Test Suite — from NIST, Microsoft, IBM, Sun, Boeing and Saxonica — with every group the suite prescribes for both versions read as each, 10,511 runs in all:
| XSD 1.0 | XSD 1.1 | |
|---|---|---|
| valid schemas accepted | 99.8% (4,563 / 4,573) | 99.8% (5,238 / 5,248) |
| invalid schemas rejected | 77.4% (171 / 221) | 69.7% (327 / 469) |
The gap is the honest description of what this is. xsdkit reads real
schemas well; it enforces some of the specification's validity constraints
and not others — which ones,
rule by rule — so a schema it accepts is not thereby a valid schema. If you
need a conformance checker, use Xerces or Saxon; if you need to read a schema
that already works, this is built for that.
Document validation is the other half of the suite — 41,994 runs over 21,671 documents:
| XSD 1.0 | XSD 1.1 | |
|---|---|---|
| valid documents accepted | 99.8% (11,300 / 11,325) | 99.5% (11,845 / 11,906) |
| invalid documents rejected | 99.7% (9,057 / 9,083) | 98.4% (9,524 / 9,680) |
git clone --depth 1 https://github.com/w3c/xsdtests /tmp/xsdtests
export XSDTESTS=/tmp/xsdtests
cargo test --test w3c_suite -- --nocapture # both halves, ~35 seconds
Both halves score against a committed per-case baseline in
tests/conformance/, so a regression names the case it broke rather than
moving a percentage. CI runs it on every push.
Security
Schemas arrive from elsewhere as often as documents do.
- No network by default.
FileResolverrefuseshttp(s)://; supply your ownResolverto opt in. - No external entities. Not a setting —
roxmltreeperforms no I/O, so they cannot be fetched. Internal DTD subsets are accepted, because real schemas use them (the W3C's own among them), with entity-reference-loop detection closing the billion-laughs vector. - Bounded work. A per-document node cap (
nodes_limit), an include-nesting cap, and cycle guards on every graph walk. - Fuzzed. Four
cargo-fuzztargets cover the loader, the pattern transpiler, value parsing and instance validation, seeded from the W3C suite. Every finding has a named regression test; seefuzz/.
Design
DESIGN.md reviews the XSD format and 17 implementations across 8 languages, and lays out the staged plan this crate follows.
License
MIT
Release files for xsdkit 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xsdkit-0.2.0.tar.gz | 458.6 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| xsdkit-0.2.0-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| xsdkit-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl | CPython 3.9 | abi3 | Linux musl 1.2+ x86-64 | Details |
| xsdkit-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl | CPython 3.9 | abi3 | Linux musl 1.2+ ARM64 | Details |
| xsdkit-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| xsdkit-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| xsdkit-0.2.0-cp39-abi3-macosx_11_0_arm64.whl | CPython 3.9 | abi3 | macOS 11.0+ ARM64 | Details |
| xsdkit-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl | CPython 3.9 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 12.9 MB
Release files / xsdkit-0.2.0.tar.gz
| Download URL | xsdkit-0.2.0.tar.gz |
|---|---|
| Size | 458.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
906f4e924def4fd5314ac2bf15b49aab598626208f3ed441abfa036b8f06b2fd
|
|
BLAKE2b-256 checksum How to use checksums |
c8e084b1c70051514b1add1a939e39cd05843fdf185c91eb116cb210ce9be335
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / xsdkit-0.2.0-cp39-abi3-win_amd64.whl
| Download URL | xsdkit-0.2.0-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 1.5 MB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
dd71ba1b7680e9ea390caa469283de33600383e9ec0e95115cc646a29487747d
|
|
BLAKE2b-256 checksum How to use checksums |
277663e78dbfcda6a212d4ee34a5e93fb95f2c4ed421cead595661b04e683593
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / xsdkit-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl
| Download URL | xsdkit-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.1 MB |
| Tags | CPython 3.9 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
6ff955317d587b59be380052498c338efecc89e215c6988cfcfb3068886c2d75
|
|
BLAKE2b-256 checksum How to use checksums |
7e424bc6b5fb8ec2a9a257192f31dda425d80f1806e2384096cfc571982ef8cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / xsdkit-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl
| Download URL | xsdkit-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.0 MB |
| Tags | CPython 3.9 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
ca5a5cb61686ec0978033bcaddfe37cc7ef7fcc460f99f3f10942f70d39b4446
|
|
BLAKE2b-256 checksum How to use checksums |
b9356f01a88e9f4b219a6395094d8f0d415429fa8ec91bccfb504aa02bb65378
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / xsdkit-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | xsdkit-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 1.8 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
c7ba92e446d7ac86aac023ee4f83969bed6b41aa82b3a16986e2b15f7ae3ba68
|
|
BLAKE2b-256 checksum How to use checksums |
83a67d5e57f79a2ae24611d3a6620472f024c099b30d4976ced057994eb14706
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / xsdkit-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | xsdkit-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 1.8 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
c2465b58407924fe9d236bb8b8a0fa2871292ca21ccce691fc1e4db190b49f9f
|
|
BLAKE2b-256 checksum How to use checksums |
1621bcb781d33beb83830f9142dcb588c01a1922e3e35d24867a0a1b6481a97c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / xsdkit-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | xsdkit-0.2.0-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.6 MB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
af22cd2743c8e4c1e6e8fcabcc9a8cfc44c03176ebd6a92720f72b807372fedd
|
|
BLAKE2b-256 checksum How to use checksums |
9ab82e17244832506029ea43649c284808ea5cffc63209c63d464e12780b1bfd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / xsdkit-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | xsdkit-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.7 MB |
| Tags | CPython 3.9 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
49ec4a9e2a1d0ee0eba7bb0046fa8f1605ad298aa8b1aa87d4183b0114d8c04d
|
|
BLAKE2b-256 checksum How to use checksums |
8d64fc959b2fe56c816d248eddc35710f8fb3e27cd860d8dc94a8fa7f84116df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency log