xsd-tools: marshalling code in five languages from one XSD
If you work with XML or JSON against an XSD schema, you've almost certainly hand-written the boring half: the code that walks a document into structs and back out again. It's tedious, it's repetitive, and it's exactly the kind of code that grows subtle bugs — an attribute read into the wrong field, a bound that's checked in the spec but not in the parser.
The generators that exist to spare you this are each welded to one
language — JAXB for Java, xsd.exe for .NET, generateDS for
Python. So the moment a schema is shared across services written in
different languages, you're back to maintaining N separate, slowly
drifting hand-rolled implementations of the same contract.
xsd-tools takes a different cut at it: one schema in, idiomatic marshalling code out, for ten targets across five languages — and it's open source. Today it ships its 0.2.0 release.
One schema, every binding
xsd-tools reads an XSD and runs it through a template that emits the target-language code. The built-in targets cover the C / C++ / Python / Java / TypeScript grid across both XML and JSON:
| Language | XML | JSON |
|---|---|---|
| C | expat (SAX + DOM) | json-c |
| C++11 | expat | json-c |
| Python | stdlib xml.sax | stdlib json |
| Java | JDK StAX | org.json |
| TypeScript | fast-xml-parser | — |
Every one of those targets is round-trip tested in CI — generate the code, compile it, marshal a document, unmarshal it, and check it survives — on Linux, macOS, and Windows, across x86-64 and arm64. Change the schema once and regenerate, and the bindings stay in sync by construction.
The validation is generated too
This is the part that pays for itself. xsd-tools doesn't just map elements to fields — it carries the schema's constraints into the generated code. Take a rating bounded to 0–5:
<element name="rating">
<simpleType>
<restriction base="int">
<minInclusive value="0"/>
<maxInclusive value="5"/>
</restriction>
</simpleType>
</element>
The C/JSON target narrows that field to the smallest type that fits and emits the bound as a parse-time check — an out-of-range document is rejected, not silently accepted:
typedef struct {
uint8_t Content_; /* 0..5 narrows to uint8_t */
} json_rating;
/* in the generated unmarshaller: */
if (!((pObj->Content_) >= 0 && (pObj->Content_) <= 5))
/* facet violation — reject the document */
The same restriction becomes a Short with a range guard in
Java, and the equivalent narrowed type and check in every other target.
Range, length, and enumeration facets — on elements and attributes
alike — are all enforced in the code the tool writes for you.
xs:documentation rides along too, emitted as a comment at
the matching type, field, or attribute.
What makes it different
- Five languages, one schema. Ten round-trip-tested targets, kept consistent by regenerating rather than by discipline.
- Extend without touching C++. Output is driven by small Lua templates, not compiled into the core. Adding a new target — a different language, a different library, your own house style — is a template file, not a fork and a rebuild.
- Constraints are enforced, not just documented. Facet validation and type narrowing, as above.
- Real-world schemas. Namespaces and
targetNamespace,xs:import/xs:include, substitution groups, abstract types, and recursive (self- and mutually-referential) types are all handled. - CLI or library. Use the
xsdbcommand, or embed the generator in your own C++ viaXsdTools::Generate().
Honest caveats
This is an early (0.2.0) release. It's round-trip tested across a wide grid, but it's young — expect rough edges and a moving API.
- Some XSD constructs are parsed but don't affect output.
Identity constraints (
key/keyref/unique),anyAttribute,notation,nillable, andxsi:typeruntime substitution are accepted so real schemas load, but they don't shape the generated code.redefineis skipped outright. A capability map spells out exactly what's supported, ignored, or rejected. - Pattern facets aren't enforced yet — regex
patternrestrictions are parsed but not turned into checks. Range, length, and enumeration are. - Imports resolve from the local filesystem only. An
http://schemaLocationfails cleanly rather than fetching.
Getting it
The 0.2.0 release attaches prebuilt Linux packages — a Debian
.deb, an RPM .rpm, and a single-file Flatpak
bundle — and builds from source via CMake on Linux, macOS, and Windows.
Point it at a schema and a target and the code prints to stdout:
xsdb python-sax library.xsd # generated Python to stdout
xsdb --out-dir out c-json-jsonc library.xsd # write the C/JSON files
xsdb --list # show the available targets
xsd-tools on GitHub 0.2.0 release
Open source and actively developed — issues and PRs welcome.