Canonicalization & equivalence (§8.3)
Two independent implementations only interoperate if they agree on when two records mean the same thing. OpenBody defines this precisely (§8.3): reduce each record to a canonical byte string via an ordered, deterministic algorithm, then compare those strings. Two records are equivalent iff their canonical byte strings are identical — regardless of JSON key order, whitespace, number spelling, or the permitted shorthands.
This page explains the procedure plainly. The normative version is SPEC §8.3; it is grounded on RFC 8785 (JSON Canonicalization Scheme).
Round-trip = parse → canonicalize → serialize
A “lossless round-trip” is: parse the record, reduce it to canonical form, serialize. An implementation demonstrates conformance by round-tripping the test vectors and comparing against this canonical form.
The ordered algorithm
- Numbers → exact-decimal fixed-point. Every number is read from its decimal text,
never via binary float (so
37.4220is exactly37422 × 10⁻³, not afloat64approximation), and replaced by its lowest-terms fixed-point object with stringcoefficient/exponent. So72,72.0, and{coefficient: 720, exponent: -1}all become{"coefficient":"72","exponent":"0"}. Because they’re strings, there’s no 2⁵³ precision ceiling — arbitrary-precision decimals compare exactly. Timestamps are likewise canonicalized to a single spelling (uppercaseT/Z,Zfor zero offset, trailing-zero fractional seconds removed). - Canonicalize units. A metric
unitequal to the field’s default is removed (sotime: 120andtime: {absolute:{value:120, unit:"s"}}converge). Aunitinsideload.valueis moved to its canonical home,Load.unit. - Expand scalar metrics. A bare scalar
nbecomes{ "absolute": { "value": n } }for every metric field and forload.value. - Expand & fold
ExerciseRef. A bare-string ref becomes{ "id": … }; an explicitopenbody:prefix on a canonical id is folded to the unprefixed form. - Expand
sets. Aprescriptionwithsets: Nis replaced by N siblingWorkUnits. (AWorkUnitcarrying bothsetsandperformanceis invalid.) - Assign deterministic ids (root-down). Any record still lacking an
idgets<nearestAncestorId>#<containerField>#<index>(e.g.ex-1#workUnits#3).#is reserved in producer ids, so assigned ids never collide. - Flatten containment. Each inlined child becomes a standalone record with an explicit
partOflink to its parent; containment arrays are removed.subjectand the nearest ancestor’sstartTime/endTimepropagate down (an explicit value on the child wins). - Default
status. Absentstatus→active. - Serialize canonically. Order the three set-valued arrays —
links,effortLoad,modifiers— by their keys (ties broken by canonical bytes); all other arrays keep their semantic order. Then serialize per RFC 8785: lexicographic key sort, canonical escaping, no insignificant whitespace.
The set of canonical record byte strings (one per flattened record) is compared as an unordered set.
Nested ≡ flat + partOf
A direct payoff of flattening (step 7): the §5 hierarchy can be encoded two equivalent ways —
a nested document (a child inlined in its parent, the recommended transmission form) or
flat + partOf (a child as a standalone record linking to its container). A consumer
MUST NOT treat them as different activities, and the conformance vectors assert one
structure’s two encodings equivalent.