Canonical JSON — RFC 8785 and Why Key Order Matters
Canonical JSON (RFC 8785 / JCS) is a byte-stable serialization of JSON. Two semantically identical objects always produce the same bytes and the same hash.
- RFC 8785 (JCS) defines a deterministic byte representation for any JSON value.
- Sorted object keys, no insignificant whitespace, and a fixed number format make hashes reproducible.
- NakedPnL canonicalizes every venue response before computing contentHash so verifiers can re-check it.
Definition
Canonical JSON, formally specified as the JSON Canonicalization Scheme (JCS) in RFC 8785, is a byte-stable serialization of any JSON value. Given the same logical input, every conforming implementation produces the same byte string. The standard fixes the things JSON normally leaves underdefined: object keys are sorted in lexicographic order of their UTF-16 code units, insignificant whitespace is removed, strings use a fixed escaping rule, and numbers are emitted using the IEEE 754 round-trip representation produced by ECMAScript Number.prototype.toString.
Why key order matters for hashing
Cryptographic hash functions hash bytes, not meaning. Two JSON serializations of the same object can differ in key order, whitespace, or number format, and those textual differences will produce completely different SHA-256 digests. If NakedPnL hashed venue responses verbatim, a verifier who pretty-printed the same data, or whose JSON parser sorted keys differently, would get a different digest and conclude the data was tampered with even though it was not. Canonicalization eliminates that false-positive class.
How NakedPnL uses it
Inside `lib/calculation/audit-hash.ts`, every raw venue response is passed through a JCS canonicalizer before it is hashed. The result is the byte string that goes into `contentHash = SHA-256(canonicalize(rawResponse))`. The canonical bytes are also retained in storage so a third party performing independent verification at `/verify/chain/[handle]` can re-canonicalize and re-hash without having to guess which serialization NakedPnL used.
Worked example
// Two semantically identical JSON objects, two different byte strings:
const a = '{"asset":"USDT","free":"10250.00","locked":"0.00"}';
const b = '{ "free": "10250.00", "locked": "0.00", "asset": "USDT" }';
// SHA-256(a) !== SHA-256(b) because the bytes differ.
// After RFC 8785 canonicalization, both become:
const canonical = '{"asset":"USDT","free":"10250.00","locked":"0.00"}';
// SHA-256(canonical) is identical for both, regardless of input order or whitespace.Common pitfalls
- Number precision: 1.0, 1, and 1e0 are all the same number but serialize differently. JCS fixes this by mandating the ECMAScript Number toString output.
- Unicode normalization: JCS does NOT apply NFC normalization. Strings that differ only in Unicode form are considered different. NakedPnL relies on venues returning consistent ASCII identifiers.
- Floating point: JCS rejects NaN, Infinity, and -Infinity. NakedPnL ensures venue responses contain only finite numbers before canonicalization.
- Key duplication: JSON technically allows duplicate keys; JCS does not. NakedPnL parsers reject duplicate keys outright.
Related terms
- Content hash — the SHA-256 digest computed over canonical JSON.
- Hash chain — chains contentHashes derived from canonical JSON inputs.
- SHA-256 — the digest function applied to canonical bytes.
- JWS Detached / JCS Signing — IETF standards that build on JCS for signed JSON.