Skip to content

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.