The same data, a different string
{"a":1,"b":2} and {"b": 2, "a": 1} represent identical data — JSON objects are explicitly unordered by spec. But if you hash both strings, or compare them for equality, you get two completely different results, because a hash function and a string comparison don't know or care about JSON semantics — they only see bytes. This becomes a real problem the moment you need to sign JSON, cache it by content hash, or compare two payloads for genuine equality: whitespace, key order, and number formatting all change the bytes without changing the meaning.
What canonicalization actually does
JSON Canonicalization takes any JSON value and produces exactly one, deterministic serialization of it — the same input always produces the identical output string, no matter which language or library generated the original JSON. RFC 8785 defines the standard version, known as JCS (JSON Canonicalization Scheme).
// Input A
{"b": 2, "a": 1, "c": [3, 2, 1]}// Input B — same data, different order and spacing
{
"c": [3, 2, 1],
"a": 1,
"b": 2
}// Both canonicalize to the exact same string:
{"a":1,"b":2,"c":[3,2,1]}Note: JCS sorts object keys, but preserves array order — arrays are ordered by definition in JSON, so [3, 2, 1] stays [3, 2, 1], not sorted to [1, 2, 3].
The rules that make it deterministic
JCS specifies exact, unambiguous rules for every part of serialization:
| Aspect | Rule |
|---|---|
| Object keys | Sorted by their UTF-16 code unit values |
| Whitespace | None — no spaces, no newlines |
| Numbers | Serialized per ECMAScript's Number.toString() rules (no trailing zeros, no + on exponents) |
| Strings | Escaped per a fixed minimal rule set — only what must be escaped is escaped |
| Unicode | Normalized encoding for identical characters |
The numbers rule is the subtle one: 1.50, 1.5, and 1.5e0 are the same numeric value but different source text. JCS defines exactly how a number must be re-serialized so every implementation that follows the spec produces the identical digit sequence.
Why this matters for signatures
A digital signature works by hashing a payload and signing the hash — but that only proves anything if the *verifier* hashes the exact same bytes the *signer* did. If a payload is re-serialized anywhere along the way (a proxy re-encodes it, a library re-orders keys), the hash changes and a perfectly legitimate payload fails signature verification. This is precisely why JSON Web Signatures used with detached content, several Web Payments specs, and some blockchain/verifiable-credential systems mandate JCS: it guarantees that "the same data" really does mean "the same bytes" everywhere.
// npm install canonicalize
import canonicalize from "canonicalize";
import { createHash } from "crypto";
const payload = { b: 2, a: 1 };
const canonical = canonicalize(payload); // '{"a":1,"b":2}'
const hash = createHash("sha256").update(canonical).digest("hex");
// Any other system canonicalizing the same logical data gets the same hashOther places "canonical JSON" shows up
- Content-addressed storage — systems that derive a content ID from a hash of the JSON (a pattern used in some verifiable credentials and decentralized storage formats) need canonicalization so logically-identical documents produce the same address.
- Idempotency / deduplication keys — hashing a canonicalized request body is a more reliable dedup key than hashing the raw request text, which can vary by client formatting.
- Diffing and testing — canonicalizing two JSON payloads before comparing them turns "are these semantically equal" into a simple string-equality check, sidestepping key-order false negatives.
- Cache keys — deriving a cache key from canonical JSON avoids cache misses caused purely by irrelevant key reordering upstream.
Canonicalization is not encryption or compression
It's worth being explicit about what JCS is *not*: it doesn't hide, shrink, or protect data — it only makes the *serialization* deterministic. The data is exactly as readable (or sensitive) as before; you still need TLS for transport security and proper encryption for confidentiality. Canonicalization solves a narrow, specific problem: "do these two logically-equal documents produce identical bytes."