JSON¶
Model mapping¶
JSON is the baseline. An object becomes a list of edges, in the order its keys appear. A key whose value is an array becomes a repeated label: the array is not a value in the model, it is the same label occurring more than once.
| JSON | Document |
|---|---|
{"a":1,"b":2} |
[(a,1),(b,2)] |
{"m":[A,B]} |
[(m,A),(m,B)] |
{"m":[A]} |
[(m,A)] — one edge, indistinguishable from {"m":A} |
No temporal types. JSON has none, so a reader MUST NOT produce date,
time, or datetime on its own. A date-looking string stays a string unless a
schema upgrades it in stage 2. A writer MUST stringify a temporal leaf to
ISO-8601.
No NaN or Infinity. Those tokens are not valid JSON. A writer MUST NOT
emit them. The default behavior is to substitute null at the leaf so the
output is always valid JSON, and to report the substitution as an
error-severity adjustment in the format report. A strict mode MAY instead fail.
Bare nested arrays are rejected. [[1,2],[3,4]] has inner elements with no
label and therefore no edge to occupy. A reader MUST reject it rather than
flatten it.
Top level. A JSON document may have many top-level keys, which becomes many top-level edges. That is legal, and it is also the shape XML cannot carry.
Interleaving is lost on write. Edges sharing a label are grouped into one
key regardless of position, because a JSON object cannot express
[(m,A),(x,X),(m,B)]. See §7.3.
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 JSON:
{"order": {"id": "A1", "status": "shipped", "total": 29.97,
"address": {"street": "1 Main", "city": "London"},
"items": [{"sku": "W", "qty": 3, "price": 9.99},
{"sku": "G", "qty": 1, "price": 9.99}]}}
reads 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) ]) ]) ]
The two items objects arrive as two edges sharing the label items, which is
exactly what "items" [1,]: LineItem counts. Writing back reverses it: two
items edges regroup into one JSON array. Had there been one line item, the
writer would emit a bare object rather than a one-element array, by the count-1
rule in §7.3.
total is 29.97 and reads as a number with no schema involved. qty reads
as an integer. Neither needs materialization; the schema's role here is to
check shape, not to convert. A JSON leaf that did need conversion — a
datetime field carrying "2024-01-01T12:30:00" — would stay a string after
stage 1 and only become a datetime in stage 2.
A real-world example. The running Order schema is clean by design — it
has no unions and no open key sets, so it never has to confront JSON's actual
worst case. package.json does: its author/bugs/repository fields can
each be either a string or an object, and scripts/dependencies are
genuinely open key sets. Modeling it is exactly the kind of case any (§3.7)
exists for — see package.osd and
its two fixtures in ../examples/.
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 every codec.
There is no JSON-specific entry in §9.4. The general divergences that also apply here are D-1 (resource caps unverified outside Python) and D-4 (no implementation emits §8.3 error codes yet), neither of which is about JSON.