3. The JSON form
The JSON form is what CRUBAL stores, sends and signs. It holds the same pairs as the text form (chapter 2), with nothing added: a Reader MUST convert either form into the other without losing a pair (comments and layout are not pairs).
JSON here is RFC 8259 JSON in UTF-8. A document MUST NOT contain duplicate member names, and MUST NOT contain
null anywhere.
3.1 A record #
{
"id": "P-4QX7",
"kind": "item",
"level": "product",
"of": "G-2HM8",
"owner": "mediaencyclopedia.com",
"wears": ["OpticalDisc", "Movie"],
"created": "2026-10-13T18:02:11Z",
"updated": "2026-10-14T09:40:03Z",
"values": {
"title": "National Lampoon's Christmas Vacation",
"format": "DVD",
"releaseDate": "1997-10",
"runtime": { "$qty": "97", "unit": "min" },
"videoCodec": { "$empty": "unknown" },
"hdrFormats": { "$empty": "n/a" },
"genres": ["Comedy", "Holiday"],
"rating": [{ "$value": "PG-13", "region": "US" }],
"work": { "$link": "W-81CC" },
"director": { "$link": "wd:Q311319" },
"cast": [
{ "person": { "$link": "wd:Q310394" }, "character": "Clark Griswold" },
{ "person": { "$link": "wd:Q229048" }, "character": "Ellen Griswold" }
],
"ebay:Studio": "Warner Home Video"
},
"parts": [
{ "$row": "disc1", "wears": ["Disc"], "values": { "discFormat": "DVD-9" } }
],
"sources": {
"*": { "origin": "reported", "via": "L-3A0K" },
"runtime": { "origin": "observed", "via": "L-7Q1F" },
"cast[wd:Q229048].character": { "origin": "declared", "via": "L-3A0K" }
}
}
3.2 Members of a record #
| Member | JSON | Required |
|---|---|---|
id |
string, chapter 1.1's ID grammar | yes |
kind |
string | yes |
level |
string | yes |
of |
string, an ID | when the record is attached (chapter 1.2); absent when it stands alone |
owner |
string, a domain or a hosted person's domain:ID (chapter 14.1) |
yes |
within |
string, an ID | when the record belongs to another for access (chapter 14.2) |
status |
{"state": "active"} or {"state": "archived", "reason": "sold", "into"?: ID} |
when archived; absent means active |
applied |
object: issuer → integer (chapter 10.7) | when the record has applied an instruction |
wears |
array of strings (class names, in order) | when the record wears more than its base class |
created · updated |
string, RFC 3339 in UTC with Z |
yes in a store; MAY be left out at a border |
values |
object: key → value (3.3) | yes; {} when the record has no values |
parts |
array of part objects (3.6) | when there are parts |
sources |
object: address → source (3.7) | in full form; left out in plain form |
alternatives · suggestions |
object: address → array of values with sources (chapter 8.4) | when there are any; left out in plain form |
held |
object: address → a held value and what holds it (chapter 8.8) | when a value is held; left out in plain form |
A member not in this table MUST be refused. Packages extend records with facets, never with members.
3.3 Values #
The facet decides the shape (chapter 1.4). Where JSON has the shape, the value is plain JSON. Where it
doesn't, the value is a tagged object: an object with exactly one member whose name starts with $,
plus the plain members that tag allows. Because no key starts with $ (chapter 1.3), a tagged object can
never be mistaken for a compound.
| Shape | JSON | Example |
|---|---|---|
| text · choice | string | "DVD" |
| number | string holding chapter 1.4.2's decimal | "97", "2.50" |
| quantity | {"$qty": decimal string, "unit": string} |
{"$qty": "97", "unit": "min"} |
| money | {"$money": decimal string, "currency": ISO 4217 code} |
{"$money": "12.50", "currency": "USD"} |
| date | string: YYYY, YYYY-MM, YYYY-MM-DD, or with a time YYYY-MM-DDThh[:mm[:ss]] and its offset (Z when zero) |
"1997-10", "2026-11-02T14:30Z" |
| time of day | string: hh:mm or hh:mm:ss |
"14:30" |
| yes-no | true · false |
true |
| link | {"$link": target} |
{"$link": "wd:Q311319"} |
| image | {"$image": location} plus optional sha256, w, h (integers) and type |
{"$image": "https://…", "w": 1600} |
| unknown | {"$empty": "unknown"} |
|
| does not apply | {"$empty": "n/a"} |
|
| qualified | {"$value": any value but an empty, …qualifiers} |
{"$value": "PG-13", "region": "US"} |
| compound | object: child key → value | {"height": {"$qty": "7.5", "unit": "in"}} |
| raw (unknown key) | the JSON value as received; from text, a string | "Warner Home Video" |
Numbers are strings. Every number, quantity amount and money amount is a decimal string, never a JSON
number, so no reader can round it through binary floating point. (This tightens the blueprint's example,
which showed 97 as a JSON number.) The string MUST match chapter 1.4.2's grammar.
Qualifiers are the members of a $value object other than $value. Each is a key and a value of its
own facet's shape; a qualifier's value MUST NOT be qualified or empty.
3.4 Many values #
- A many-valued facet's value is always an array, even with one item. A single-valued facet's value is never an array.
- An empty array MUST NOT be written; it is the same as absent.
- An item MUST NOT be an empty (
$empty). An array MUST NOT hold two equal items (chapter 1.11). - An ordered list keeps its order. A set's order means nothing, and canonical form sorts it (3.9).
3.5 Rows #
A many-valued compound is an array of row objects.
- When the compound has an identity child, the row key is derived from it and
$rowMUST NOT be written. - Otherwise every row MUST carry
"$row": rowkey, unique within the array. $rowis the only$member a row may have.
3.6 Parts #
{ "$row": "disc1", "wears": ["Disc"], "values": { "discFormat": "DVD-9" } }
A part object has exactly $row (required), wears (when it wears classes) and values (required). A
lower-level record mirrors its parts by $row and holds only the values that vary at its level; it MAY leave
wears out.
3.7 Sources #
sources maps an address (chapter 1.10) to a source object:
{ "origin": "observed", "via": "L-7Q1F", "owner": "someone-else.org" }
originis one ofobserved,declared,derived,reported.viais a log entry ID. Both are required.owneris left out when it is the record's owner.adopted: truemarks a value made one's own (chapter 8.2).- The address
*is the record's default source. A more specific address wins: a list item over its list, a compound leaf over its compound, a value over*. - Every stored value MUST be covered by some source,
*or more specific. An address that names nothing invaluesorpartsMUST be refused. - Addresses into parts start with
parts[rowkey].
3.8 Other documents #
Each document type is a JSON object with a distinct identifying member.
Intake record. One object as it arrived through a receive contract (chapter 9).
{
"intake": "I-55ZT",
"contract": "me:ebayCatalog /2026-10-01",
"received": "2026-10-14T09:39:58Z",
"for": "P-4QX7",
"raw": { "type": "application/json", "sha256": "4be1…", "body": "{\"Run Time\":\"97 min.\"}" },
"outcomes": [
{ "from": "Run Time", "to": "runtime", "outcome": "absorbed", "value": { "$qty": "97", "unit": "min" } },
{ "from": "Studio", "to": "ebay:Studio", "outcome": "unknown" },
{ "from": "Format", "to": "format", "outcome": "contradiction", "value": "Blu-ray",
"cites": { "value": "parts[disc1].discFormat" } }
]
}
raw.bodyis the payload's exact bytes: a string when they are valid UTF-8, otherwiseraw.base64instead.raw.sha256is the hash of those bytes. A payload MUST NOT be parsed and re-serialized for storage.arrival, when the payload had to find its record (chapter 9.2a):{"outcome": "bound" | "matched" | "held" | "new", "ref": "…", "by": "identify:asin"}, pluscandidateswhen held.foris absent while an arrival is held.outcomeis one ofabsorbed,new,conflict,contradiction,unknown,ignored. AcontradictionMUST carrycites, naming exactly one oftype(the facet),rule,value(an address on the same object),global(a global rule and the other record) orslot(a part slot, chapter 5.6). Anewthat is a unit question carriescitesnamingunit, the spelling (chapter 11.9).
Log entry. One write, hash-linked to the owner's previous entry (chapter 8).
{
"log": "L-7Q1F",
"prev": "sha256:2c77…",
"owner": "mediaencyclopedia.com",
"at": "2026-10-14T09:40:03Z",
"by": { "contract": "me:ocr", "run": "R-118" },
"intake": "I-56AA",
"source": { "origin": "observed" },
"writes": [
{ "record": "P-4QX7", "address": "runtime", "value": { "$qty": "97", "unit": "min" } },
{ "record": "P-4QX7", "address": "title", "value": "Christmas Vacation (DVD)", "as": "alternative",
"source": { "origin": "reported" } },
{ "record": "U-9KD2", "envelope": "applied", "value": 3 }
],
"sig": "…"
}
as is omitted when it is current, and a write carries its own source only when it differs from the
entry's (chapter 8.5).
by names exactly one of person, contract (with run), rule, migration, instruction or take (with from,
chapter 8.5). A write that removes a value has
"removed": true in place of value. A write taken out by redaction is {"redacted": "sha256:…"}
(chapter 8.5.2).
Observation. An evidence-backed claim made on one instance (chapter 9).
{
"obs": "O-2KQ0",
"on": "U-9KD2",
"address": "runtime",
"value": { "$qty": "97", "unit": "min" },
"evidence": { "$image": "https://…/back-cover.jpg", "sha256": "77a0…" },
"evidence-type": "photo",
"reading": "I-56AA",
"supports": "P-4QX7"
}
Border wrapper. Data crossing a border travels in a wrapper, the only place a version appears:
{
"crubal": "2",
"package": "mediaencyclopedia.com/me /2026-10-13",
"prefixes": { "me": "mediaencyclopedia.com/me", "wd": "https://www.wikidata.org/entity/" },
"records": [ … ]
}
prefixes maps every prefix the data uses to its full identity (chapter 11.1), so a receiver resolves
me:videoCodec without knowing the sender's nicknames. The wrapper MAY also carry intake, log and
observations arrays.
3.9 Canonical form #
For hashing and signing, a document is put in canonical form and then serialized by RFC 8785 (the JSON Canonicalization Scheme):
- every set is sorted by the RFC 8785 serialization of its items, in code-unit order;
- ordered lists keep their order, and rows of a set are sorted like any item;
wearskeeps its order;- numbers are strings already, so RFC 8785's number rules never apply;
- no defaults are filled in and no empties are added: what is absent stays absent.
Two documents with the same canonical bytes hold the same data.
3.10 Text ↔ JSON #
| Text (chapter 2) | JSON |
|---|---|
file header crubal, package |
the border wrapper |
file header owner |
each record's owner, unless the record says otherwise |
header level: id of parent |
level, id, of; kind from the package |
| header detail | sources["*"] |
crubal:wears, crubal:parts, crubal:created, crubal:updated, crubal:owner |
the envelope members |
key: value |
values[key] |
| a value's source detail | sources[address] |
| a value's qualifier detail | a $value object |
[row: key] |
$row |
| image fields in a detail | members of the $image object |
A Reader MUST produce the same canonical JSON (3.9) from a text and from its full-mode print, and MUST print the same pairs back from any JSON it read.