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.
Same JSON data, different SHA-256 hashes
Consider these two JSON documents.
{"name":"Alice","age":25}And:
{
"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.
First:
45ca59b78c209d8bec5e7aa355d217c1a110c9105297bf641f8e090115bc0262
Second:
b0e10df3fdc5294f0452661d6688ad08e095180049b39fa694bd6f5db06a6138Both 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.
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
{"name":"Alice"}{"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
{"name":"Alice","age":25}{"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
{"letter":"A"}{"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:
{"a":1,"b":2}{"b":2,"a":1}But these arrays do not mean the same thing:
["first","second"]["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.
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.
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.
{"b":2,"a":1}That document is already minified, but it is not in JCS property order.
{"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.
{"price":4.50}{"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.
{
"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.
{
"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().
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.
{
"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.
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.
{
"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:
{"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:
NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9XsThe 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.
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.
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.
{"name":"Alice","age":25}{
"name": "Alice",
"age": 25
}{"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.
- 2.
- 3.RFC 7638 — JSON Web Key (JWK) Thumbprint
RFC Editor
- 4.RFC 7515 — JSON Web Signature (JWS)
RFC Editor
- 5.CVE-2022-25757 — Apache APISIX Request Validation Bypass
National Vulnerability Database
- 6.CVE-2022-25757: Apache APISIX — body_schema check bypass
Apache Software Foundation / oss-security