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} |
{"m":[]} |
zero m edges — the label is simply absent from the result |
No temporal types. JSON has none (C-1),
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, unaltered and with its seconds, and report format.temporal-stringified
(E-37).
No NaN or Infinity. Those tokens are not valid JSON. Reading one MUST
fail with parse.codec-syntax (C-19).
Writing one MUST
(E-6)
fail with write.unsupported-value
(§8.3.8),
unconditionally — not gated behind a strict mode, and never by substituting
null. A substituted NaN and a genuine null produce the identical JSON
token and read back as the identical Document value, so the substitution could
never be detected or undone afterwards.
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.
An empty array is a legitimate zero-edge encoding, not an error.
{"m":[]} reads to zero m edges: the label simply doesn't appear in the
result, the same as if the key were absent entirely. This is JSON-specific
and does not extend to OML's [...] array sugar (OML-11) — a bare m: [] in OML MUST
be rejected (verified live: empty array is not allowed), because OML's
array syntax is sugar for a run of same-label edges and a zero-length run
is indistinguishable from no sugar being used at all. JSON has no such
ambiguity: its arrays are a real container independent of the Document
model's edge-repetition mechanism, so an empty one is just as meaningful as
a non-empty one.
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.
Duplicate keys: the last value wins, at the first position
(C-11). {"a":1,"a":2}
reads as [(a,2)] — one edge, not two — and {"a":1,"b":2,"a":3} reads as
[(a,3),(b,2)]: the surviving edge sits where the key was first written and
carries the value written last. The JSON grammar itself is silent on duplicate names (RFC 8259
permits but discourages them and does not define a resolution), so this is an
Omnist policy choice, not a JSON requirement — chosen because it is already
the de facto behavior of essentially every mainstream JSON parser (a later
key overwriting an earlier one when built into a map), so codifying it costs
nothing across ports and avoids inventing divergent behavior where none
currently exists in practice. This is unrelated to a repeated label
becoming a repeated edge ({"m":[A,B]} → [(m,A),(m,B)], see above) — that
is JSON's own array syntax, not a duplicate-key situation. Duplicate keys
collapse to one edge; a JSON array under a single key does not. YAML and TOML
do not take this policy: their own specifications call a repeated key an error,
and Omnist rejects it there (C-12).
A number too large for binary64 reads as an infinity. {"a": 1e999} reads
as the number +Infinity and -1e999 as -Infinity, with no diagnostic
(D-29); a literal smaller than the
smallest subnormal reads as 0.0. A Document holding an infinity cannot then be
written back as JSON, since the format has no token for it (see above). An
integer literal, one with no fraction and no exponent, is an integer however
long it is, and D-28 bounds its
digits.
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 on which implementations ship a JSON codec. This page states no per-port status of its own: a copy here can only go stale relative to the ledger.
There is no JSON-specific entry in §9.4 — see that section for whatever's currently open.