Canonicalize JSON (RFC 8785) and hash it
To hash JSON reliably, first turn it into one canonical form: sort object keys, remove whitespace between tokens, and write numbers and strings exactly as the JSON Canonicalization Scheme (RFC 8785) defines. Then hash the UTF-8 bytes of that form with SHA-256. The tool on this page does both in your browser and shows the exact bytes it hashed.
- Updated
- Reading
- 4 min
- The need
- Get the same hash for the same JSON data, no matter which program wrote it or how it was formatted.
- Plain text
- /guides/json-canonicalization.md
{"checkpoint":"4d711cb37270e1422b5c50b04de3a47881f309a23bff4d7e4e6374b3041b0580","day":"2026-10-20","prev":"79d4944aa54bf14eea6d7ab3e76d0b693bccf7fdca26528396585d7832565a91","root":"efbf6979b0dbd40f344ba886cf446c298192ac6dba53687948d516d6ce57c775","rules_hash":"5ae2f2cfcd61bbec8e4dc3b15ef96397e01890fbe103e26fc59b61044d7c359c","t":"d","tip":"685fbd254262c954d19cd40e75b37420ba231c5862178ed04577327a631f986d","v":2}- Bytes (UTF-8)
- 415
- SHA-256 of prefix + canonical
#Why JSON needs a canonical form
Two JSON documents can carry exactly the same data and still differ byte for byte. {"a":1,"b":2} and { "b": 2, "a": 1 } mean the same thing to every parser, but their SHA-256 hashes have nothing in common. Key order, spaces, line breaks, 1.0 against 1, and "\u00e9" against "é" all change the bytes.
That matters whenever a hash or a signature has to be checked by someone else. If the writer and the checker serialize the same object differently, an honest document fails verification. A canonical form removes the choice: for any JSON value there is exactly one way to write it.
#How RFC 8785 makes JSON canonical
The JSON Canonicalization Scheme fixes every decision a serializer could make:
- No whitespace between tokens.
- Object keys sorted by their UTF-16 code units, which is how JavaScript sorts strings by default. Uppercase sorts before lowercase, and
"é"sorts after"z". - Strings escaped the way ECMAScript's
JSON.stringifydoes it: only the characters that must be escaped are escaped, and everything else is written as UTF-8. - Numbers written the way ECMAScript turns a number into text.
2.50becomes2.5,-0becomes0,1e21becomes1e+21, and0.000001stays0.000001. - Duplicate keys are rejected. Most parsers keep the last value silently, which would let two people read different data from the same text.
Press "Try a tricky example" above to see each rule at work.
#Doing it yourself
In JavaScript the whole scheme fits in a few lines, because the language already serializes strings and numbers the way RFC 8785 asks:
function jcs(v) {
if (v === null || typeof v !== "object") return JSON.stringify(v);
if (Array.isArray(v)) return "[" + v.map(jcs).join(",") + "]";
return "{" + Object.keys(v).sort()
.map((k) => JSON.stringify(k) + ":" + jcs(v[k])).join(",") + "}";
}
async function sha256Hex(text) {
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
Two things this short version leaves to you: rejecting duplicate keys, which JSON.parse resolves silently by keeping the last one, and refusing values JSON cannot carry, such as NaN or Infinity. The tool on this page does both. In other languages, use a library that states RFC 8785 conformance rather than a general "sorted keys" option, because number formatting is where implementations quietly differ.
#Common mistakes
- Hashing the pretty-printed file. Hash the canonical form, not the text you happen to have on disk.
- Sorting keys by locale.
localeCompareand database collations do not sort by UTF-16 code units, so"Z"and"a"can swap places. - Floats in hashed data. RFC 8785 handles them correctly, but other systems may round the same value differently before it reaches you. If the numbers are money or scores, store integers in a fixed unit instead (see the guide on deterministic numbers).
- Forgetting the encoding. Hash UTF-8 bytes. Hashing a UTF-16 string gives a different result.
- A domain prefix you didn't apply. Many systems hash
prefix + canonicalso a hash from one context can't be replayed in another. Upvera's day files useupvera/day/v1followed by a newline.
#Questions
Is RFC 8785 the same as sorting keys with JSON.stringify?
Almost, in JavaScript. A recursive serializer that sorts keys and uses JSON.stringify for strings and numbers produces RFC 8785 output. In other languages the number formatting usually differs, so use a conforming library.
Does canonical JSON change my data?
No. It changes only how the data is written. Parsing the canonical form gives back the same values, with duplicate keys refused.
Why not just hash the bytes I received?
You can, if you will always keep those exact bytes. A canonical form lets anyone rebuild the bytes from the data itself, which is what makes independent verification possible.
Can I use SHA-256 on its own as a signature?
No. A hash shows that data is unchanged compared with a hash you already trust. To show who produced it, sign the hash, or publish it somewhere that can't be quietly rewritten, as Upvera does with its day roots.
#How Upvera does it
Every Upvera day file is canonical JSON with eight fields. Its digest is sha256("upvera/day/v1\n" + canonical), and the next day's file carries that digest as prev. The example loaded in the tool is the real day file of the date shown; with the prefix switched on, the hash it computes is the prev value of the following day on GitHub. Upvera's own records use a stricter subset of RFC 8785 in which every number is an integer. The full chain is in the Day Roots report.