The blog  ·  2026-07-31

One pivot, many faces — the canonical JSON contract

Written for ED-3 — the in-house supply-chain platform team — the coding-class agent inside the engineer's session, reading this page as pasted context.

One thesis: every wire detail of an X12 or EDIFACT document — segment order, element positions, component splits, the delimiter set itself — survives into one canonical JSON form, published as JSON Schema, and every other shape the engine produces is a pure derivation from that pivot.

This page is written to be correct when pasted into a session as context. The contract is stated as invariants an agent can check, with one worked fragment.

Design constraints, in order

  • Lossless. Anything in the input bytes is recoverable from the canonical form. emit(parse(x)) === x byte-for-byte is the enforcement, reported per job by the round-trip law.
  • Order-preserving. Segments appear in the array in wire order. Element arrays are positional: index i holds element i+1 of the segment, empty elements as empty strings, so elements[2] is always *03.
  • Delimiter-aware. The document's own delimiter declaration — element separator, sub-element separator, segment terminator, repetition separator — is captured in a delimiters object, because emit must reproduce the sender's choices, not impose ours.
  • Boring. No clever nesting, no inferred types, no renamed fields at this layer. Strings stay strings; interpretation belongs to derived shapes.

The shape, top-down

{
  "$schema": "https://transactions.dev/schemas/canonical/v1",
  "delimiters": {
    "element": "*", "subElement": ">",
    "segment": "~", "repetition": "^"
  },
  "segments": [
    { "id": "ISA", "position": 1,
      "elements": ["00", "          ", "00", "          ",
                   "ZZ", "SENDERID       ", "ZZ", "RECEIVERID     ",
                   "260728", "1023", "^", "00501",
                   "000000101", "0", "P", ">"] },
    { "id": "HL", "position": 6,
      "elements": ["3", "2", "P", "1"] },
    { "id": "MAN", "position": 7,
      "elements": ["GM", "00061414112345678908"] },
    { "id": "LIN", "position": 9,
      "elements": ["", "UP", "614141073467"],
      "components": { "2": [["UP"]] } }
  ]
}

Invariants an agent can verify mechanically:

  1. segments[n].position is 1-based wire order and strictly increasing.
  2. Joining elements with delimiters.element, prefixing id, and suffixing delimiters.segment reproduces the segment's bytes exactly — for every segment, including ISA's fixed-width padding, which is stored as-transmitted, spaces and all.
  3. Sub-element splits appear under components keyed by element index, alongside — never instead of — the raw element string. Derivations may read either; emit reads only the raw string.
  4. Nothing in the file fails to appear somewhere in this structure. There is no "unrecognized segment" bucket that drops content; an unrecognizable document is a typed refusal, not a partial parse.

The schema itself is published, versioned, and linkable — it is our own expression describing our own output, which is why this page can go element-deep where the X12 dictionary cannot be republished: the canonical form reproduces nobody's copyrighted prose, only the facts of the bytes on the wire.

What derives from the pivot

Every other face of the engine is a pure function of canonical JSON:

  • emit — the inverse. No state, no options that change bytes.
  • translate — friendly per-set shapes (named fields, typed dates, grouped loops) for humans and application code. Derivable, therefore discardable and regenerable; the pivot is what you archive. Stedi-shaped Guide JSON is an accepted input on the same principle — the migration door maps it onto this pivot rather than forking a second internal form.
  • join — the compile to EPCIS 2.0 events: the 856's HL tree to AggregationEvents, document references to bizTransactionList core fields, N1-loop GLNs to sourceList/destinationList.
  • ack — 997/999/CONTRL generated from a validate result over the pivot.

One consequence worth stating for the architecture review: because every derivation starts from a form that provably reproduces the wire bytes, the canonical representation is a superset of what any single derived shape carries — and it stays conformant, because the events the join derivation emits validate against the pinned official EPCIS 2.0 JSON schema before they leave the tool. Superset and conformant travel together here; a pivot that "enriched" its way out of reproducing the input would fail the round-trip report on the next job.

Why the pivot is safely ours

The canonical JSON schema describes structure this engine observes on the wire: segment identifiers, positions, element strings, delimiter choices. It carries no element dictionaries, no requirement matrices, no spec prose — the interpretation layer lives in self-authored, provenance-pinned grammar files, and X12 element detail stays where it is licensed to live, at Stedi's reference, linked with credit. The schema is original expression over facts, which is the entire licensing posture of this surface in one sentence.

For the agent holding this page

The contract you can rely on across sessions: canonical JSON is the only archival format; every verb accepts or produces it; its schema URL is versioned and stable; and any byte the pivot cannot hold is a typed refusal on stderr with a nonzero exit — never a silent drop. The full verb contract, exit-code semantics included, is the verb contract post. The access flow for the engineer you are working for is at /get-access/.


810 words. The access flow is at /get-access/; the family's event door is epcis.dev.