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.1"
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.1", 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.
  • 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, 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
→ XSD 1.1 assertions and conditional type assignment next
Identity constraint enforcement

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 (5,727 scored schema cases from NIST, Microsoft, IBM, Sun, Boeing and Saxonica):

valid schemas accepted 99.7% (5,231 / 5,247)
invalid schemas rejected 66.7% (320 / 480)

The gap is the honest description of what this is. xsdkit reads real schemas well; it does not yet enforce most of the specification's validity constraints, 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 — 21,575 scored cases, 99.0% correct:

valid documents accepted 99.5% (11,846 / 11,907)
invalid documents rejected 98.4% (9,517 / 9,668)
git clone --depth 1 https://github.com/w3c/xsdtests /tmp/xsdtests
export XSDTESTS=/tmp/xsdtests
cargo test --test w3c_suite -- --nocapture              # schemas, seconds
cargo test --release --test w3c_suite -- --ignored --nocapture   # documents

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.1.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.1.0
File Size Uploaded
xsdkit-0.1.0.tar.gz 394.2 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for xsdkit 0.1.0
File
xsdkit-0.1.0-cp39-abi3-win_amd64.whl CPython 3.9 abi3 Windows x86-64 Details
xsdkit-0.1.0-cp39-abi3-musllinux_1_2_x86_64.whl CPython 3.9 abi3 Linux musl 1.2+ x86-64 Details
xsdkit-0.1.0-cp39-abi3-musllinux_1_2_aarch64.whl CPython 3.9 abi3 Linux musl 1.2+ ARM64 Details
xsdkit-0.1.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.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.9 abi3 Linux glibc 2.17+ ARM64 Details
xsdkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details
xsdkit-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details

Total release size: 12.4 MB

Release files / xsdkit-0.1.0.tar.gz

Download URL xsdkit-0.1.0.tar.gz
Size 394.2 kB
Tags Source
SHA-256 checksum
How to use checksums
2981d0bea49e12890005a2ad835a7f5b93f540725b09a5980436397083ab6b7c
BLAKE2b-256 checksum
How to use checksums
fbb1c5c9bbcf22a3ba25446e20b44017fee7d50e4008c2e391927e163f17d56f
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 7, 2026.

Transparency log

Release files / xsdkit-0.1.0-cp39-abi3-win_amd64.whl

Download URL xsdkit-0.1.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
f8cb2fb5b21e620337709bb4e85d6a39a2c7606df89735f99866a61ddafeecd2
BLAKE2b-256 checksum
How to use checksums
673e0652275126399b4b6d1256414b35caa42d6248fba22c5aed910b48ae4416
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 7, 2026.

Transparency log

Release files / xsdkit-0.1.0-cp39-abi3-musllinux_1_2_x86_64.whl

Download URL xsdkit-0.1.0-cp39-abi3-musllinux_1_2_x86_64.whl
Size 2.0 MB
Tags CPython 3.9 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
202868d4d329b76e173d04b5ff9150cba4ad17f39c2205fcb11deb9f5182a989
BLAKE2b-256 checksum
How to use checksums
a07a63bdfd5031559dcf2f42fb9d9c011f22a8dbe8ea898cd0f0f02868e09fdc
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 7, 2026.

Transparency log

Release files / xsdkit-0.1.0-cp39-abi3-musllinux_1_2_aarch64.whl

Download URL xsdkit-0.1.0-cp39-abi3-musllinux_1_2_aarch64.whl
Size 1.9 MB
Tags CPython 3.9 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
807c76e6898de23c0e0a2d6dc7bb92e1694733ec7299240368d2c4a005c7c1aa
BLAKE2b-256 checksum
How to use checksums
e96f8238a7ab58937481f308924a8e2c0766d87daa0de70aa2a8fb56abf4af98
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 7, 2026.

Transparency log

Release files / xsdkit-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL xsdkit-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 1.7 MB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
8aa388a5f477c717c7ef0632c162be529ec5f53721636b085b3826cc1a75a064
BLAKE2b-256 checksum
How to use checksums
d8a210da05599ef61a0341e891e0a3f0e06b1a96d2b5b907d0cf312c527cf6bb
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 7, 2026.

Transparency log

Release files / xsdkit-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL xsdkit-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 1.7 MB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
079179fc533aef850476479c2dd9307ee836a640349ac48fb22b5ce79303e461
BLAKE2b-256 checksum
How to use checksums
558fc84de8dfd076c343baccab451a7e569e69fccc51211c3b75ef4f95d06150
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 7, 2026.

Transparency log

Release files / xsdkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl

Download URL xsdkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
Size 1.5 MB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
b155aafb41ffba1caaefe9cd60079f119ea3cf3b4cbf1c7345c8912aceed1e03
BLAKE2b-256 checksum
How to use checksums
ec6d1c399d84fd559ffe520774d4aa3867af1fca76d97889b8f65fabad823d81
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 7, 2026.

Transparency log

Release files / xsdkit-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl

Download URL xsdkit-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl
Size 1.6 MB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
ad7b7dee3d3567e36222e77ea68b9319dff0d3082dcb7dd19d318638da6ec3a1
BLAKE2b-256 checksum
How to use checksums
472e133b481f42a07b6f27d1ac9f52304b8f2ee5509d9672aab959b68067bd6c
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 7, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.0

8 release files

0.2.0

8 release files

This release

0.1.0 This release

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