Skip to content
Guides

7 JSON Canonicalization Checks for Reliable Hashes

·10 min read
7 JSON Canonicalization Checks for Reliable Hashes - Guides guide from JSON Prettifier

Quick answer: JSON canonicalization converts supported JSON data into a predictable serialized representation under an agreed scheme. Use it when producers and consumers must hash equivalent data consistently. For webhooks signed over the original body, preserve those original bytes instead. Formatting, minification, and canonicalization solve different problems.

Your webhook arrives, the JSON looks correct, and verification still fails. Or two services calculate different cache keys for what appears to be the same object. Before changing your cryptography, check what each service actually hashes. These seven checks separate representation problems from authentication problems.

Use JSON Compare to inspect the values in two sample payloads. Formatting or comparison does not replace raw-body signature verification or canonicalization.

1. Are you hashing JSON values or the original bytes?

Hashing JSON values requires a shared representation rule because equivalent values can arrive in different text forms. Hashing an original request body requires preserving that body unchanged. Decide which contract your system uses before parsing or formatting anything; otherwise, a harmless-looking transformation can change the input and invalidate verification.

Start with a simple distinction. An object containing a quantity and a product identifier describes data. The HTTP body carrying that object is a sequence of bytes. Those bytes also contain punctuation, whitespace, and the chosen spelling of numbers and escapes. A checksum sees the representation it receives, rather than your interpretation of the business fields.

The following executable example uses Node's built-in crypto module. Both documents contain the same fields and values, but they produce different SHA-256 digests. The assertion compares digest outputs; it does not implement a secure authentication protocol.

import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
const hash = text => createHash('sha256').update(text, 'utf8').digest('hex');
const first = '{"product":"book","quantity":2}';
const second = '{ "quantity": 2, "product": "book" }';
assert.notEqual(hash(first), hash(second));
assert.equal(JSON.parse(first).quantity, JSON.parse(second).quantity);

Make the distinction explicit in a design document: are you authenticating a received message, deduplicating a business event, or building a content-addressed document store? Those tasks can require different inputs. A delivery identifier can be a better deduplication key than any payload digest when a provider already guarantees stable event identifiers.

For broader context, Wikipedia's canonicalization overview explains the general idea of choosing a standard representation. It is background, not an implementation specification. Our JSON comparison guide covers value-level comparisons when cryptographic verification is not the goal.

2. Does your webhook provider require the unchanged request body?

A webhook provider that signs the raw body requires verification against the unchanged payload it sent. Do not replace that payload with parsed and reserialized JSON, even when the values look identical. Preserve the original body at your framework boundary, and use the provider's verifier with its documented headers and secret.

Stripe's signature troubleshooting documentation explains that body mutation can break verification. It also distinguishes endpoint secrets for CLI-forwarded requests from Dashboard-managed endpoints. A body representation problem is therefore only one possible cause of a failure; the wrong secret can cause the same visible symptom.

Capture the original body before a general JSON parser consumes it. In an application with several routes, route-specific middleware order matters. Write down which component first reads the request stream, whether it decompresses anything, and what type it returns. Avoid debugging by logging complete production bodies containing payment details or personal data.

A useful troubleshooting sequence is:

  1. Confirm that the verifier receives the original body.
  2. Confirm that the signature header is present and unchanged.
  3. Confirm that the endpoint secret belongs to that delivery environment.
  4. Check timestamp and replay rules using the provider's SDK.
  5. Investigate proxies and middleware if the previous checks pass.

The generic hash example above deliberately omits these authentication details. Copying it into a webhook handler would leave critical checks missing. Use our JSON security guide for the wider processing boundary and sensitive-data redaction guide for safer diagnostics. Pretty-print only a separate, sanitized copy after verification.

3. Have both services agreed on a canonicalization scheme?

Reliable canonical JSON hashes require both services to implement the same scheme, including supported values, property ordering, serialization, and output encoding. A label such as stable JSON is not a complete contract. Name the specification, record the implementation you use, and compare exact output bytes across every language in the integration.

The JSON Canonicalization Scheme, RFC 8785, provides one defined approach. It combines constrained input data, serialization rules, and deterministic property sorting. Do not infer interoperability merely because two libraries both advertise sorted keys. Their treatment of number spelling, invalid Unicode, or unsupported values can differ.

Ask the receiving team for a known input document and its expected canonical output. Keep that output as a fixture in source control. Then calculate its digest independently. When a disagreement appears, compare the canonical bytes first; debugging the signature algorithm before confirming its input makes the problem harder to isolate.

GoalAppropriate representationFirst thing to compare
Read an API responseFormatted JSON copyParsed fields
Verify a raw-body webhookOriginal received bodyUntouched payload and provider inputs
Sign canonical JSONAgreed canonical formExact canonical UTF-8 bytes
Compare business recordsApplication-defined normalized dataDomain rules and selected fields

Keep application normalization separate from canonicalization. Removing an optional field, converting a date to another timezone, or treating missing values as null changes the data model. A canonical serializer cannot decide whether those business changes are acceptable. Our JSON date-format guide and null-versus-undefined guide help identify those upstream decisions.

4. Are nested objects sorted while array positions stay unchanged?

Canonicalization must apply its property-order rules throughout nested objects, including objects inside arrays, without changing the array's element order. Sorting only the root object leaves nested differences unresolved. Sorting arrays introduces a different error by changing the data. Test both behaviors independently before relying on a canonical representation for hashes.

Imagine an order with a shipping-address object and an array of processing steps. The address properties can appear in different text orders without changing their meaning. The processing steps cannot be rearranged safely: charge, ship, and refund describe a sequence. A serializer that sorts every container loses that distinction.

RFC 8785 defines recursive object-property sorting while preserving array order. Its property comparison rule uses UTF-16 code units rather than a locale-specific language sort. That detail matters when multiple runtimes generate the same signed document. Avoid substituting a user-facing alphabetical comparator whose result changes with locale settings.

This small assertion illustrates why sorting the root alone is insufficient. It is a failure demonstration, not a canonicalization implementation:

import assert from 'node:assert/strict';
const left = { address: { city: 'Ahmedabad', zip: '380001' } };
const right = { address: { zip: '380001', city: 'Ahmedabad' } };
const rootOnly = value => JSON.stringify(value, Object.keys(value).sort());
// A replacer array filters nested keys too: both addresses lose their fields.
assert.equal(rootOnly(left), '{"address":{}}');
assert.equal(rootOnly(right), '{"address":{}}');

This common shortcut is especially dangerous because matching outputs can look like success. In fact, the example discards data. Inspect the serialized result, not just whether two hashes match. Use the JSON viewer to inspect a safe sample and the nested JSON guide to understand the structure before choosing a library.

5. Can your parser preserve the numbers and strings you intend to sign?

A canonical serializer cannot restore information that a parser has already lost. Check numeric precision, duplicate property handling, and string validity before treating parsed data as a signing input. Represent identifiers outside your runtime's exact integer range as agreed strings, and reject ambiguous input rather than silently choosing an interpretation.

Consider an external account identifier delivered as a JSON number. JavaScript's ordinary number type cannot represent every large integer exactly. After parsing, a different identifier can already be in memory. Serializing that value consistently does not repair the loss. The ECMAScript Number specification describes the underlying numeric type; our JSON precision guide explains application-level options.

import assert from 'node:assert/strict';
const parsed = JSON.parse('{"account":9007199254740993}');
assert.equal(parsed.account, 9007199254740992);
const safe = JSON.parse('{"account":"9007199254740993"}');
assert.equal(safe.account, '9007199254740993');

Strings deserve the same attention. Two visually similar Unicode sequences need not contain identical code units. Do not silently normalize them unless your application contract explicitly requires normalization before signing. Keep the original spelling of identifiers, names, and opaque tokens intact when those strings are part of the authenticated data.

Duplicate names create another boundary problem. A parser can discard an earlier occurrence, leaving a single property that conceals the ambiguous source. Consult I-JSON, RFC 7493 for interoperability constraints and our duplicate JSON keys guide for practical detection considerations. Validation must happen early enough to see the original ambiguity.

6. Do you hash the same encoding and use the right authentication method?

Matching JSON text is only useful when both sides feed matching bytes into the cryptographic operation. Specify UTF-8 for canonical output and document digest encoding separately. A hexadecimal digest and a Base64 digest can represent the same result differently. Plain hashes also provide no sender authentication; use the protocol's required authentication mechanism.

The Node crypto documentation describes hashing, HMAC, and timing-safe comparison APIs. A hash calculates a digest without a secret. An HMAC uses a shared secret. A digital signature uses an asymmetric key arrangement. Pick the operation required by your protocol rather than treating those terms as interchangeable.

A useful local fixture can include all of the following: source JSON, expected canonical text, expected canonical bytes, expected digest bytes, and the encoded digest supplied to the other service. Comparing each stage reveals whether the disagreement concerns serialization, character encoding, hashing, or output formatting.

Avoid trimming strings as a general repair. A newline at the end of a raw signed body is part of that body. Conversely, a protocol defining canonical output without extra whitespace must not receive a newline added by a file-writing utility. The correct behavior comes from the contract, not from an assumption that whitespace never matters.

For authenticated messages, comparisons and error handling also belong to the protocol. A constant-time comparison API does not automatically make the surrounding handler timing-safe. Different buffer lengths, decoding errors, replay handling, and early exits still need deliberate treatment. Prefer a maintained verifier over a custom cryptographic path when the provider supplies one.

7. Have you tested failures as well as matching outputs?

A useful canonicalization test suite checks both equivalent inputs that should match and changed inputs that must differ. Include nested properties, array reordering, numeric boundaries, Unicode, and rejected inputs. Run shared fixtures in every producer and consumer. Successful round trips alone cannot reveal a serializer that accidentally drops fields or accepts ambiguous data.

Start with positive fixtures: reorder object properties, vary irrelevant JSON whitespace, and construct equivalent nested objects independently. Then introduce meaningful changes: move an array element, modify a value, replace a string with a number, or remove a field. Every meaningful change should receive the outcome your contract specifies.

Add rejection fixtures for inputs your selected scheme does not support. Check what happens before parsing, during canonicalization, and during verification. A test that never reaches the intended validation boundary tells you little about that boundary. Keep the expected rejection explicit rather than treating any exception anywhere as success.

For cross-language integrations, run the same fixture files in continuous integration. Pin and review library changes because a dependency upgrade can affect compatibility even when your own code stays unchanged. Store the scheme name alongside the format contract, so a later engineer can tell why the fixtures exist.

The JavaScript snippets in this guide were executed as assertions during preparation. They demonstrate byte differences, a lossy sorting shortcut, and integer precision loss; they do not claim to implement RFC 8785. Before production signing, verify your chosen implementation against the specification's examples and your integration's own fixtures. Use the JSON formatting guide for inspection, while keeping the authenticated representation under its original rules.

Frequently Asked Questions

Is JSON canonicalization the same as minification?
JSON canonicalization applies an agreed set of serialization rules to produce a repeatable representation of supported data. Minification removes unnecessary whitespace. Two minified objects can still have different property orders, so their hashes can differ. Choose canonicalization when a protocol requires it; choose minification when reducing document size is the goal.
Should I canonicalize a Stripe webhook before verification?
Do not canonicalize a Stripe webhook before signature verification. Stripe verification expects the unchanged request body together with the signature header and endpoint secret. Parsing, sorting keys, or formatting that body can change the signed input. Use Stripe's documented verifier first, then process the verified event according to your application's rules.
Does JSON.stringify sort every object key alphabetically?
JSON.stringify does not alphabetically sort every object key. JavaScript property enumeration gives integer index keys special ordering and preserves insertion order for other ordinary string keys. Serialization can therefore be repeatable for one unchanged object without matching another equivalent object created differently. An agreed canonicalization scheme addresses that separate cross-producer requirement.
Can a hash prove who sent JSON?
A plain hash cannot prove who sent JSON because anyone can compute a hash of a chosen message. Authentication requires a protocol such as a message authentication code using a shared secret or a verified digital signature. Canonicalization makes the input representation predictable; it does not supply identity or secret keys.
Can I sort arrays to make JSON hashes match?
Do not sort arrays just to make JSON hashes match. Array order is part of JSON data and can represent priority, chronology, or a sequence of actions. JSON Canonicalization Scheme sorts object properties while preserving array positions. If your application treats a collection as unordered, define that normalization separately in its contract.

Want to inspect your JSON before testing hashes?

Format a non-sensitive sample to inspect its structure. Keep the original signed payload unchanged for verification.

Open JSON Prettifier