Skip to content

Why Two JSON Files Can Mean the Same Thing but Have Different Hashes

Why can equivalent JSON produce different SHA-256 hashes? See how whitespace, key order, line endings, serialization, and JSON canonicalization change the bytes being hashed.

By Updated

Same JSON data, different SHA-256 hashes

Consider these two JSON documents.

JSON
{"name":"Alice","age":25}

And:

JSON
{
  "age": 25,
  "name": "Alice"
}

A program can parse both into an object containing the same two members: name is Alice and age is 25.

But if you calculate SHA-256 over the text exactly as written, the results are different.

Text
First:
45ca59b78c209d8bec5e7aa355d217c1a110c9105297bf641f8e090115bc0262

Second:
b0e10df3fdc5294f0452661d6688ad08e095180049b39fa694bd6f5db06a6138

Both hashes are correct. The apparent contradiction disappears once you separate the meaning of the JSON from the bytes used to represent it.

SHA-256 does not know that the input is JSON

A JSON parser understands structure. It recognizes objects, arrays, strings, numbers, booleans, and null.

SHA-256 does not.

A hash function receives bytes. It does not know that a pair of quotation marks contains a property name or that a space after a colon is insignificant to a JSON parser.

What SHA-256 actually sees
Input
JSON text
Bytes
Bytes
Crypto
SHA-256
Defined
256-bit digest

Change the input bytes and you have changed the message being hashed. Even if a parser later turns both messages into equivalent data, the raw SHA-256 calculation has already seen two different inputs.

Four small changes that can change a JSON hash

You do not need to change the actual data to change a raw hash. Differences that are almost invisible to a person can be enough.

1. Whitespace

JSON
{"name":"Alice"}
JSON
{"name": "Alice"}

The second version contains an extra space. A JSON parser can produce the same object from both, but the raw text is different.

2. Object key order

JSON
{"name":"Alice","age":25}
JSON
{"age":25,"name":"Alice"}

JSON objects are collections of name/value pairs. The two examples contain the same members, but their bytes appear in a different order.

3. Line endings and trailing newlines

A file ending with a newline is not byte-for-byte identical to one that ends immediately after the final brace. Likewise, LF and CRLF line endings use different byte sequences.

4. Different JSON spellings of the same value

JSON
{"letter":"A"}
JSON
{"letter":"\u0041"}

A parser can turn both string values into the same character, but the source text is different. Raw hashing preserves that difference.

Key order is different from array order

There is an important distinction between JSON objects and arrays.

These objects can represent the same member mappings:

JSON
{"a":1,"b":2}
JSON
{"b":2,"a":1}

But these arrays do not mean the same thing:

JSON
["first","second"]
JSON
["second","first"]

Array position is part of the data. Reordering array elements changes the value rather than merely changing its presentation.

That distinction matters when a system tries to create one deterministic representation of JSON.

There are three different meanings of 'the same JSON'

A lot of confusion comes from using the word same without saying what kind of sameness we mean.

Three ways JSON can be the same
Bytes
Same bytes
Equivalent
Exactly the same serialized input
Process
Same parsed data
Equivalent
Different text can produce equivalent structure
Defined
Same canonical form
Defined
Equivalent data is converted to deterministic bytes

If you are verifying that a downloaded file is exactly the file a publisher released, byte-for-byte sameness is usually what you want.

If you are asking whether two JSON documents represent equivalent application data, a structural comparison may be what you want.

If a cryptographic system needs equivalent JSON data to produce the same digest, then it needs a defined canonical representation before hashing.

Canonicalization gives JSON one deterministic representation

Canonicalization turns data that can be written in several valid ways into one representation produced according to fixed rules.

Canonical JSON hashing
Input
JSON input
Process
Parse and validate
Step
Canonicalize
Input
Deterministic JSON
Bytes
UTF-8 bytes
Defined
SHA-256 or another cryptographic operation

RFC 8785 defines one such system: the JSON Canonicalization Scheme, usually shortened to JCS.

JCS does more than remove pretty printing. It defines how primitive values are serialized, removes insignificant whitespace, recursively puts object properties into a deterministic order, and finally generates UTF-8 bytes.

Array elements stay in their existing order because their positions are meaningful. Objects inside those arrays still have their properties canonicalized.

If two implementations follow the same canonicalization rules on compatible input data, they can independently produce the same byte sequence for cryptographic processing.

Canonicalization is not just minifying and sorting keys

A common shortcut is to describe canonical JSON as 'remove spaces and sort the keys.' That captures part of the idea, but it is not enough to implement RFC 8785 correctly.

JSON
{"b":2,"a":1}

That document is already minified, but it is not in JCS property order.

JSON
{"a":1,"b":2}

JCS also defines serialization behavior for strings and numbers and imposes I-JSON constraints on the data being canonicalized.

Even property sorting has a precise definition. JCS compares property-name strings using UTF-16 code units rather than relying on whatever locale-aware sorting function an application happens to use.

For ASCII-only property names, the result usually looks like the ordinary alphabetical order people expect. The exact rule matters once interoperability becomes the goal.

Numbers are one of the easiest places to get this wrong

The same mathematical value can have more than one valid JSON representation.

JSON
{"price":4.50}
JSON
{"price":4.5}

Those strings are different even though a parser can produce the same numeric value.

RFC 8785 therefore defines deterministic number serialization instead of preserving the original spelling.

Large numbers need even more care. JCS operates on the I-JSON data model, which constrains JSON numbers to values representable as IEEE 754 double-precision numbers.

JSON
{
  "userId": 9007199254740993
}

That integer is beyond JavaScript's exact integer range for the ordinary Number type. If every digit must survive exactly, an application may need to represent the value using another agreed representation, such as a string, rather than silently passing it through a number type that cannot preserve it.

JSON
{
  "userId": "9007199254740993"
}

JSON.stringify() is useful, but it is not JCS

In JavaScript, a natural first attempt is to parse the data and call JSON.stringify().

JavaScript
const value = {
  name: "Alice",
  age: 25,
};

const json = JSON.stringify(value);

That is perfectly reasonable for ordinary JSON serialization.

It does not automatically turn the result into a cross-language cryptographic representation. JSON.stringify() follows JavaScript's serialization rules and preserves an object-property order determined by the JavaScript value it receives.

A canonicalization protocol deliberately defines its own property ordering and other serialization details so that those decisions are not left implicit.

If a protocol says to use JCS, JWK thumbprint rules, JWS signing input, or another exact representation, use that protocol rather than inventing a normalization step around JSON.stringify().

Duplicate JSON keys are a different and more dangerous problem

So far we have looked at different text that can represent equivalent data. Duplicate object names create a different problem: two parsers may disagree about what the data is.

JSON
{
  "role": "user",
  "role": "admin"
}

Which role should the application use?

RFC 8259 says object names should be unique. When they are not, receiver behavior is unpredictable across implementations. Some keep the last value, some reject the object, and others can expose the duplicates.

That means duplicate keys should not be treated as another harmless formatting difference.

If one component validates a message using one interpretation and a second component acts on another interpretation, the disagreement can cross a security boundary.

A real example: Apache APISIX and CVE-2022-25757

CVE-2022-25757 is a useful example because the bug did not depend on SHA-256 at all. It depended on two systems disagreeing about duplicate JSON keys.

Affected Apache APISIX configurations used lua-cjson while performing request body schema validation. For duplicate keys, lua-cjson selected the last occurrence.

An upstream application could use a different parser that selected the first occurrence instead.

How parser disagreement became a validation problem
Input
One request with duplicate keys
Process
APISIX parser
Check
Interpretation A
Check
Schema validation passes
Process
Upstream parser
Risk
Interpretation B
Risk
Application acts on different data

The validation layer could therefore approve one interpretation while the application consumed another.

APISIX fixed the vulnerable path by re-encoding the JSON it had validated and replacing the forwarded request body with that representation. The upstream application then received the version APISIX had actually checked.

The broader lesson is simple: parsing is part of a security contract. If several components process the same structured message, they need compatible rules for interpreting it.

JWK thumbprints show how a protocol can define the bytes

JSON Web Key thumbprints provide a cleaner example of the opposite approach: the protocol explicitly defines what gets hashed.

RFC 7638 does not say to hash whatever JWK text happened to arrive over the network.

For an RSA key, the thumbprint calculation constructs a JSON object containing only the required members e, kty, and n. Those member names are placed in the order defined by the RFC, unnecessary whitespace is excluded, the result is encoded as UTF-8, and those bytes are hashed.

JSON
{
  "kty": "RSA",
  "n": "...full modulus...",
  "e": "AQAB",
  "alg": "RS256",
  "kid": "example-key"
}

Fields such as alg and kid are not required members of an RSA public key, so they do not participate in this thumbprint calculation.

Conceptually, the representation being hashed looks like this:

JSON
{"e":"AQAB","kty":"RSA","n":"...full modulus..."}

The modulus is abbreviated above only to keep the example readable. The actual hash input contains the complete value.

For the complete RSA example published in RFC 7638, the SHA-256 thumbprint encoded with base64url is:

Text
NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs

The important pattern is not the particular key. It is the order of operations: first define the exact representation, then hash it.

JWK thumbprints and JCS are not the same protocol

It would be easy to see the previous example and conclude that every cryptographic JSON system should simply run RFC 8785 before hashing.

That is not the rule.

RFC 7638 defines its own representation for JWK thumbprints. RFC 8785 defines a more general JSON canonicalization scheme. They solve related representation problems, but they are separate specifications.

Cryptographic interoperability depends on all participants following the same protocol, not on everyone independently choosing a reasonable-looking normalization method.

JSON Web Signature solves the problem another way

JSON Web Signature is another useful reminder that canonicalization is not the only way to make cryptographic input precise.

RFC 7515 defines an exact JWS Signing Input. For the usual encoded-payload form, it is built from the base64url-encoded protected header, a period, and the base64url-encoded payload.

Text
ASCII(
  BASE64URL(UTF8(protected header))
  + "."
  + BASE64URL(payload)
)

JWS is not silently applying RFC 8785 to the JSON first. It defines a different byte-level construction and signs that construction.

This leads to one of the most useful rules in the entire subject: when cryptography is involved, ask what exact bytes the protocol says to authenticate.

So what should you hash?

There is no universal answer because different hashes can be intended to identify different things.

Choose the representation based on the goal
Bytes
Verify an exact file
Crypto
Hash the original file bytes
Equivalent
Check whether JSON means the same thing
Process
Parse and compare the data
Crypto
Need equivalent JSON to share a cryptographic identity
Step
Use the canonicalization or representation required by the protocol
Defined
Hash the resulting bytes

If you are checking a downloaded archive against a published SHA-256 checksum, hash the exact file bytes. Reformatting or parsing the file would defeat the point.

If you only need to know whether two JSON documents contain equivalent application data, you may not need a cryptographic hash at all. Parse them and compare the data according to the rules of your application.

If a signature, identifier, cache key, or distributed protocol requires logically equivalent JSON to produce the same cryptographic result, use a deterministic representation that every participant has explicitly agreed to.

Try the raw-hash experiment yourself

You can reproduce the basic problem with three tiny inputs.

JSON
{"name":"Alice","age":25}
JSON
{
  "name": "Alice",
  "age": 25
}
JSON
{"age":25,"name":"Alice"}

All three can parse to an object containing the same name and age values.

Hash each exact string with SHA-256 and you will get three different digests because you supplied three different byte sequences.

The Olivez Hash Generator & Checksum Checker hashes exact text locally, so it is useful for seeing this difference directly. Do not trim or reformat the examples before comparing them.

The practical takeaway

A JSON parser and a cryptographic hash answer different questions.

The parser asks what the document represents.

The hash asks which bytes it received.

That is why whitespace, property order, line endings, escape sequences, and number serialization can change a raw JSON hash even when the parsed data appears equivalent.

When exact bytes are the thing you want to identify, hash those bytes directly.

When the data itself needs a stable cryptographic representation, define that representation first. That may mean JCS, a protocol-specific construction such as a JWK thumbprint, or another explicitly specified format.

The important part is not making every JSON document look identical. It is making sure every system involved agrees on exactly what is being hashed.

Sources

  1. 1.
  2. 2.
  3. 3.
  4. 4.
  5. 5.
  6. 6.
    CVE-2022-25757: Apache APISIX — body_schema check bypass

    Apache Software Foundation / oss-security