Skip to content

XML

The data-XML profile

Omnist does not read arbitrary XML. It reads a deliberately narrow subset — data-XML — and a conformant reader MUST reject anything outside it rather than ignore the unsupported construct (E-7).

Supported. Elements, text content, CDATA sections, comments, processing instructions, the XML declaration, attributes (dropped on read, reported via format.attribute-dropped), and namespace prefixes (dropped, reported via format.namespace-dropped).

The declaration's encoding= is ignored, and the input is read as UTF-8. Whatever encoding the declaration names, the bytes are decoded as D-14 says, so a declaration that claims ISO-8859-1 neither refuses the document nor re-decodes it (C-15).

Characters and references are checked, and entities are decoded once. A character outside XML 1.0's Char, and a numeric character reference that is malformed or names such a character (�, , �, , �, &#;, A), is parse.codec-syntax, never dropped, kept as text or replaced; A reads as the text A, not as A (C-16).

Not supported. Each of these MUST fail the read (E-7):

Construct Why it is out of profile
A DOCTYPE declaration, of any kind Nothing in a DTD contributes to the resulting Document, so honouring one is attack surface for no benefit — external entities reach the filesystem and the network, and nested internal entities expand exponentially
An entity reference other than XML's five predefined (< > & " ') Its definition could only have come from a DTD, which is already refused
Mixed content — text alongside child elements in one element There is no Document shape for it: an edge's target is a value or a node, never both (§2.3 D-4)

Reject on sight, not on use (E-7). A reader MUST fail when it encounters the DOCTYPE declaration itself, not later when an entity defined by it is referenced. A document that declares entities and never uses them is still refused. This is what makes the rule testable and keeps the failure early and predictable rather than conditional on content.

These are refusals, not malformed input (E-7). Every document above is well-formed XML; Omnist is declining a feature. A reader MUST NOT report them with a code or message implying the input is invalid XML, because that sends the user looking for a defect in a file that has none.

Why this is stated at all. Before it was, the five implementations had each drawn their own line and disagreed: two rejected DOCTYPE-bearing documents, two silently skipped the declaration, one accepted it outright. That is precisely the "grammar acceptance" variation §9.2 forbids — "accepting a superset is as much a failure as accepting a subset."

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.

An empty element is the empty string, and XML has no null. An element with no child elements is a string leaf holding its text, so <a/> and <a>10](../07-codecs-and-deserialization.md#73-writing)). It MUST NOT write an empty element and report an adjustment; there is noformat.code for it. An *absent* optional field is written by omitting the element, which is what[0,1]cardinality already means; an empty string leaf still writes asboth read as(a,""). The text is kept exactly as written, whitespace included: adjustment; there is no format.* code for it. An absent optional field is written by omitting the element, which is what [0,1] cardinality already means; an empty string leaf still writes as <a/> reads as (a," "), and an element holding only a line feed as the one-character string. Whitespace between child elements is formatting and is dropped. The write side of a whitespace-only leaf cannot be pinned by a vector, because the harness comparison strips it (E-18), so the read side is pinned instead. XML has no null token to write instead, and writing a null leaf as <a/> would produce text that reads back as the empty string, a different Document. A writer MUST therefore fail on a null leaf with write.unsupported-value, unconditionally and not only under strict, at the Document path of the leaf ($.root.item[1] for the second of a repeated item), exactly as TOML does for its null (C-10). It MUST NOT write an empty element and report an adjustment; there is no format.* code for it. An absent* optional field is written by omitting the element, which is what [0,1] cardinality already means; an empty string leaf still writes as <a/>.

A typed leaf is written as its text, and the writer reports it. XML has no typed literals, so an integer, a number or a boolean leaf is written as its text (<n>5](../08-conformance-and-errors.md#832-document-building-and-limits), which states it with a worked case; no vector can check it, because theparsedriver takes no schema, so it is implemented from the text). Which texts are pretyped at all is [C-18](../07-codecs-and-deserialization.md#71-two-stages)'s: anintegerleaf only from-?(0|[1-9][0-9]*), anumberleaf only from a JSON number, and+5,007,0x1F,1_000,NaNandINFstay strings. Anumber` leaf past binary64's range reads as an infinity, with no length limit (D-29).

Attributes and namespace prefixes are dropped, and the drop MUST be reported (E-5). <a x="1">) and reads back as a string; the writer reports format.value-stringified at the Document path of the leaf. A date, time or datetime leaf is written as its ISO-8601 text (<d>2024-01-15ich texts are pretyped at all is [C-18](../07-codecs-and-deserialization.md#71-two-stages)'s: anintegerleaf only from-?(0|[1-9][0-9]*), anumberleaf only from a JSON number, and+5,007,0x1F,1_000,NaNandINFstay strings. Anumber` leaf past binary64's range reads as an infinity, with no length limit (D-29).

Attributes and namespace prefixes are dropped, and the drop MUST be reported (E-5). <a x="1">) and is reported as format.temporal-stringified instead, one code per leaf and never both (E-5). A repeated leaf is reported once per occurrence, each indexed ($.r.d[0], $.r.d[1], E-10). An integer leaf read from XML under a schema is pretyped, and the digit limit binds that route: a leaf typed integer with more digits than the limit is refused with document.limit.int-digits at the leaf's path ($.r.n for <r><n>…** ([E-5](../08-conformance-and-errors.md#838-format-codec-adjustments)).E-5). <a x="1">, indexed for a repeated leaf), in the library and in a command-line materialize alike (E-4a, which states it with a worked case; no vector can check it, because the parse driver takes no schema, so it is implemented from the text). Which texts are pretyped at all is C-18's: an integer leaf only from -?(0|[1-9][0-9]*), a number leaf only from a JSON number, and +5, 007, 0x1F, 1_000, NaN and INF stay strings. A number leaf past binary64's range reads as an infinity, with no length limit (D-29).

Attributes and namespace prefixes are dropped, and the drop MUST be reported (E-5). `hiwzxhzdk:2

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.

This is XML's documented pretyping exception, not the general stage-2 rule. Parsing that XML with no schema yields total and qty as the plain strings "29.97" and "3", because XML text carries no type information at all — `zdk:2

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.

This is XML's documented pretyping exception, not the general stage-2 rule. Parsing that XML with no schema yields total and qty as the plain strings "29.97" and "3", because XML text carries no type information at all — <qty> reads as [(a,[(b,"hi")])]; the attribute is gone, and a reader MUST report format.attribute-dropped (at the element the attribute was lost from) when it does. A prefixed tag <ns:b> reads as the local name b, with the prefix and any namespace binding discarded, reported the same way with format.namespace-dropped. 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.

This is XML's documented pretyping exception, not the general stage-2 rule. Parsing that XML with no schema yields total and qty as the plain strings "29.97" and "3", because XML text carries no type information at all — <qty>3N and stage 1 already hastotal/qty` typed, with nothing for pretyping or materialization to do. Both paths land on the same final Document; only XML needed the schema to get there, and only XML is allowed to use it that way.

Note also that <items> looks identical whether the schema wants an integer or a string. Handed the schema, an XML reader is specifically permitted (only for XML — see §7.1) to pre-type those leaves into number 29.97 and integer 3 before the standard record-shape check runs, because there is no other way for a schema-typed XML field to ever become non-string: XML has no literal syntax to distinguish "the author wrote a number" from "the author wrote a string that happens to look like one," unlike JSON/YAML/TOML, where a leaf that's still a string after stage 1 is a string because the author chose to write one, and stays a string — materialization does not coerce it (§7.2). Read the same order from JSON and stage 1 already has total/qty typed, with nothing for pretyping or materialization to do. Both paths land on the same final Document; only XML needed the schema to get there, and only XML is allowed to use it that way.

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 on which implementations ship an XML codec and on what each one currently reports. This page deliberately states no per-port status of its own: a copy here can only go stale relative to the ledger, and did.

Attribute and namespace-prefix drops MUST be reported (E-5) via format.attribute-dropped/format.namespace-dropped (§8.3.8), never silently.