Skip to content

XML

Model mapping

XML is the format the Document model was shaped around.

Elements become edges, and interleaving survives. <m/><x/><m/> reads as [(m,...),(x,...),(m,...)] in that order. A map-of-arrays model cannot represent this; the edge list can. This is the whole reason the Document is an ordered edge list rather than a map.

Repeated elements are the array. No wrapper element is invented on either side. Two <items> elements are two items edges, full stop.

Text is untyped. XML carries no type information, so every leaf arrives as a string. Typing requires a schema in stage 2. XML is the format that leans hardest on materialization.

Single document element. An XML document has exactly one top-level element, so its Document has exactly one top-level edge. A Document with several top-level edges cannot be written as XML. To share one Document across all formats, wrap the data under a single top-level key.

Attributes and namespace prefixes are dropped. <a x="1"><b>hi</b></a> reads as [(a,[(b,"hi")])]; the attribute is gone. A prefixed tag <ns:b> reads as the local name b, with the prefix and any namespace binding discarded. There is no path from a Document edge back to an attribute, so writing never produces one.

Worked example

The schema:

record Address  { "street": string, "city": string }
record LineItem { "sku": string, "qty": integer, "price": number }

record Order {
    "id":           string,
    "status":       string,
    "total":        number,
    "address":      Address,
    "items" [1,]:   LineItem,
    "coupon" [0,1]: string,
}

record Root { "order": Order }
root Root

The same order in XML:

<order>
  <id>A1</id>
  <status>shipped</status>
  <total>29.97</total>
  <address><street>1 Main</street><city>London</city></address>
  <items><sku>W</sku><qty>3</qty><price>9.99</price></items>
  <items><sku>G</sku><qty>1</qty><price>9.99</price></items>
</order>

reads, after stage 2 against the schema, to:

[ (order, [ (id,      "A1"),
            (status,  "shipped"),
            (total,   29.97),
            (address, [ (street, "1 Main"), (city, "London") ]),
            (items,   [ (sku, "W"), (qty, 3), (price, 9.99) ]),
            (items,   [ (sku, "G"), (qty, 1), (price, 9.99) ]) ]) ]

Two things about this example are XML-specific.

The single-rooted design is for XML's sake. record Root { "order": Order } exists so that the Document has exactly one top-level edge. Without it, JSON, YAML, and TOML would all still round-trip and XML alone would fail. Every other format tolerates the wrapper; XML requires it.

Stage 1 output differs here, stage 2 output does not. Parsing that XML yields total as the string "29.97" and qty as the string "3", because XML text is untyped. It is materialization against "total": number and "qty": integer that produces the numbers shown above. Read the same order from JSON and stage 1 already has them typed. The two paths converge only after stage 2 — which is precisely why the schema is applied on the read side and never on the write side.

Note also that <items> elements do not sit inside an <items> wrapper. There is no wrapper element in this profile, on either side.

A real-world example. sitemap.xml is the cleanest of the four examples in ../examples/, because it isolates a gap category none of the others do: value refinement. The schema (sitemap.osd) types changefreq as string and priority as number — OSD has no enum or range constraint — so changefreq: sometimes and priority: 1.5 both validate cleanly under OSD despite violating the sitemaps.org spec (which restricts changefreq to a fixed set of values and priority to [0.0, 1.0]), demonstrated directly by invalid-values.xml. No union, no open key set, nothing else is at play — it's a clean demonstration of exactly one limitation (see also §6.3).

Parity gaps

Chapter 9's status table (§9.3) is the authority. As of spec v0.1, its "Codecs JSON/YAML/TOML/XML" row reads "all four" for Python, TypeScript, and Rust alike — every implementation has an XML codec.

XML is also the one format with its own open divergence. D-3 records that attributes and namespace prefixes are dropped silently, with no adjustment reported — the information loss described above is not surfaced in a format report at all, so a caller has no way to learn it happened. §8.3.8 already defines codes for reporting it; emitting them is a behavior change and needs a conformance vector first. Until that lands, implementations MUST behave identically here: dropping, silently, in the same places.