> Source: https://upvera.tech/guides/json-canonicalization/
> 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.

[Guides](/guides/)Tools

Tool · · For developers · Day Roots

# Canonicalize JSON (RFC 8785) and hash it

Short answer

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.

JSON input{ "v": 2, "t": "d", "day": "2026-10-20", "prev": "79d4944aa54bf14eea6d7ab3e76d0b693bccf7fdca26528396585d7832565a91", "root": "efbf6979b0dbd40f344ba886cf446c298192ac6dba53687948d516d6ce57c775", "tip": "685fbd254262c954d19cd40e75b37420ba231c5862178ed04577327a631f986d", "checkpoint": "4d711cb37270e1422b5c50b04de3a47881f309a23bff4d7e4e6374b3041b0580", "rules\_hash": "5ae2f2cfcd61bbec8e4dc3b15ef96397e01890fbe103e26fc59b61044d7c359c" }

Canonical form (RFC 8785)

```
{"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

Prepend Upvera's day prefix `upvera/day/v1\n`

## 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:

1.  **No whitespace** between tokens.
2.  **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"`.
3.  **Strings** escaped the way ECMAScript's `JSON.stringify` does it: only the characters that must be escaped are escaped, and everything else is written as UTF-8.
4.  **Numbers** written the way ECMAScript turns a number into text. `2.50` becomes `2.5`, `-0` becomes `0`, `1e21` becomes `1e+21`, and `0.000001` stays `0.000001`.
5.  **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:

```js
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.** `localeCompare` and 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 + canonical` so a hash from one context can't be replayed in another. Upvera's day files use `upvera/day/v1` followed 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](/roots/).

## Sources

-   [RFC 8785, JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html)
-   [RFC 7493, the I-JSON message format](https://www.rfc-editor.org/rfc/rfc7493.html)
-   [RFC 8785, section 3.2.2.3: serialization of numbers](https://www.rfc-editor.org/rfc/rfc8785.html#section-3.2.2.3)
-   [Upvera day roots on GitHub](https://github.com/upvera-co/ledger-roots)

## Related

[Guide**How to verify a hash chain of daily records**Verify a real chain of daily digests, step by step, with about 20 lines of JavaScript you can run in your browser.Anyone checkingRead](/guides/verify-hash-chain/)[Explainer**Why the same calculation gives different numbers, and how to make it deterministic**Where floating-point results drift between machines and runs, and the integer, ordering and testing habits that make a calculation repeat exactly.DevelopersRead](/guides/deterministic-numbers/)[UTR-26-03**Day Roots**Sealed every minute. Rooted every day.System reportRead](/roots/)[upvera.tech**Verify Upvera**Four checks you can run in your browser right now.ToolsOpen](/verify/)
