Skip to main content

xsdkit

CI License Docs

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.3"
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.3", 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)                         # global elements this schema declares
"{urn:example}report" in schemas     # a mapping of them: dict(schemas) works too
schemas.types["{urn:example}Report"] # types and attributes have views of their own

# 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, reads the document as it validates it so memory stays flat, and carries the outcome on its .report once the loop has ended.

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:string resolves 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; whiteSpace applied before lexical parsing.
  • Composition — include, import, redefine and override, including chameleon includes, where a document with no targetNamespace is 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 as Value::Integer(42), not "42". Handles xsi:type — prefix, derivation, block and abstractness — xsi:nil, substitution groups and wildcards.
  • Identity constraints — xs:key, xs:keyref and xs:unique over their XPath subset, with keys compared in the value space — and xs:ID / xs:IDREF uniqueness 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:all gets per-member counters rather than n! regex paths.
  • XSD 1.1, opt-in via Version::Xsd11: openContent, defaultOpenContent, defaultAttributes, vc: conditional inclusion, notNamespace / notQName, xs:precisionDecimal, the relaxed xs:all rules, 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.9% (11,309 / 11,325) 99.6% (11,863 / 11,906)
invalid documents rejected 99.7% (9,057 / 9,083) 98.3% (9,515 / 9,680)
scripts/fetch-w3c-suite.sh /tmp/xsdtests     # the commit the baselines describe
export XSDTESTS=/tmp/xsdtests
cargo test --test w3c_suite -- --nocapture   # both halves, ~35 seconds
python3 scripts/check-w3c-python.py          # the documents again, through Python

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. The instance cases run a second time through the Python package, which has to reach the same verdicts. CI runs both on every push.

Security

Schemas arrive from elsewhere as often as documents do.

  • No network by default. FileResolver refuses http(s)://; supply your own Resolver to opt in.
  • No external entities. Not a setting — roxmltree performs no I/O, so they cannot be fetched. Internal DTD subsets are accepted in schemas, because real ones use them (the W3C's own among them), with entity-reference-loop detection closing the billion-laughs vector.
  • Bounded work. Per-document caps on nodes (nodes_limit) and on element nesting (max_depth), a nesting cap for instance documents, an include-nesting cap, and cycle guards on every graph walk.
  • Fuzzed. Four cargo-fuzz targets cover the loader, the pattern transpiler, value parsing and instance validation, seeded from the W3C suite. Every finding has a named regression test; see fuzz/.

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.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xsdkit 0.3.0
File Size Uploaded
xsdkit-0.3.0.tar.gz 510.9 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for xsdkit 0.3.0
File
xsdkit-0.3.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
xsdkit-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl CPython 3.10 abi3 Linux musl 1.2+ x86-64 Details
xsdkit-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl CPython 3.10 abi3 Linux musl 1.2+ ARM64 Details
xsdkit-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
xsdkit-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details
xsdkit-0.3.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details
xsdkit-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details

Total release size: 14.1 MB

Release files / xsdkit-0.3.0.tar.gz

Download URL xsdkit-0.3.0.tar.gz
Size 510.9 kB
Tags Source
SHA-256 checksum
How to use checksums
0b8a8b60195e923426fa4a9487f47d73af610f725ad4d5056ce06bd3b1ee19d4
BLAKE2b-256 checksum
How to use checksums
38c639c3e9f51cf4a6dc824cfa9f61f1ece878f756b38ec5a0be94961807804a
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 15, 2026.

Transparency log

Release files / xsdkit-0.3.0-cp310-abi3-win_amd64.whl

Download URL xsdkit-0.3.0-cp310-abi3-win_amd64.whl
Size 1.7 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
6f14f6b895a51d2e8e145d7fbfeea0d5845b7b76ce42ad02226fc84270c6500c
BLAKE2b-256 checksum
How to use checksums
1f91344fe958fb822e00eead77a87e7cbc9f502392dd62277eb29a97c6017b75
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 15, 2026.

Transparency log

Release files / xsdkit-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl

Download URL xsdkit-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl
Size 2.2 MB
Tags CPython 3.10 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
4c1e49e23604662ed6101fa0909a94182a43ffb5f7b4b70a46d4785780cbd662
BLAKE2b-256 checksum
How to use checksums
1d32b62d911bbc948ea0c0a7c9c969d66d9d49daa8ce31f51100ceb1cc9e9a68
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 15, 2026.

Transparency log

Release files / xsdkit-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl

Download URL xsdkit-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl
Size 2.1 MB
Tags CPython 3.10 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
2c26f870aca0526b432e65de50e6b7bc349625cda0034f9794a95075dc7e3180
BLAKE2b-256 checksum
How to use checksums
576dafccc7f4ef2a663111491197d39891f27bfb084b4368b78927b7846cc1c4
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 15, 2026.

Transparency log

Release files / xsdkit-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL xsdkit-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 2.0 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
72f5ea9d8eaa99c8f8b70efb04e45e21c704ced40b5b7b28e2269db2429b1e7e
BLAKE2b-256 checksum
How to use checksums
e6875cd5bd45ae3d530ba895be1e863e9849b4fca533be44f7f026f9ed1c41d9
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 15, 2026.

Transparency log

Release files / xsdkit-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL xsdkit-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 1.9 MB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
9f231e841bb4eb41c27cda3b10b8f6b781f73bfe7c808fd51e16b9ea9e201259
BLAKE2b-256 checksum
How to use checksums
9b9ca8d0efce2d86d732523be676459132c60ddc53d46852982f55ee3291dcbc
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 15, 2026.

Transparency log

Release files / xsdkit-0.3.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL xsdkit-0.3.0-cp310-abi3-macosx_11_0_arm64.whl
Size 1.8 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
8ffcb163f9013c569118837ac238452920dba51dba6590814641b8b8cd7ff574
BLAKE2b-256 checksum
How to use checksums
5042a971c752ac62bfe14224d15464702e949f81aebb00a0e2520db98098a93e
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 15, 2026.

Transparency log

Release files / xsdkit-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl

Download URL xsdkit-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl
Size 1.8 MB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
ebc8bec997a53b30e1009fea2af5c081be6529b6a258e88dfcd850676332cf0d
BLAKE2b-256 checksum
How to use checksums
38ecce21f26f51686097ecb1e08ac98e80b8e037524b231816a90f70fe100fda
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 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

8 release files

0.2.0

8 release files

0.1.0

8 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page