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.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: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.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. 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, 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-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.2.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.2.0
File Size Uploaded
xsdkit-0.2.0.tar.gz 458.6 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for xsdkit 0.2.0
File
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 log

Release 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 log

Release 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 log

Release 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 log

Release 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 log

Release 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 log

Release 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 log

Release 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

Release history Release notifications | RSS feed

0.3.0

8 release files

This release

0.2.0 This release

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