Two JSON files. Same data. Different hashes.
Consider these two JSON documents.
{"name":"Alice","age":25}And:
{
"age": 25,
"name": "Alice"
}A JSON parser can treat both as an object containing the same two members: a name of Alice and an age of 25.
Now hash the two strings exactly as they are written.
First:
45ca59b78c209d8bec5e7aa355d217c1a110c9105297bf641f8e090115bc0262
Second:
b0e10df3fdc5294f0452661d6688ad08e095180049b39fa694bd6f5db06a6138The hashes are completely different.
That sounds strange until you separate two things that programmers often treat as the same: what the JSON means, and what bytes the hash function actually receives.
A hash does not know that it is looking at JSON
A JSON parser reads the text and turns it into structured data. It knows that name is a property, that 25 is a number, and that the spaces around JSON punctuation are not part of the data itself.
SHA-256 does none of that.
SHA-256 never sees an object called Alice. It sees bytes. That distinction is the whole story.
If the bytes change, the input to SHA-256 changes. It does not matter whether the changed byte was a meaningful character, an indentation space, a line ending, or something else that a JSON parser would normally ignore.
That is why a JSON parser can consider two documents equivalent while a cryptographic hash considers them completely different inputs.
One space is enough
You can see the distinction with an almost absurdly small experiment.
{"name":"Alice"}Now add one space after the colon.
{"name": "Alice"}Both are valid JSON and both represent the same object.
But the strings contain different bytes, so their raw SHA-256 hashes are different.
The same thing happens with a trailing newline. A person looking at a text editor might barely notice it. A hash function cannot ignore it unless the application deliberately removes it before hashing.
This is also why an exact-text hash tool should not quietly trim the input. If the purpose is to hash the supplied text, changing that text first would change what is being hashed.
Key order creates the same problem
Whitespace is not the only difference that can disappear when JSON is parsed.
{"name":"Alice","age":25}{"age":25,"name":"Alice"}These two objects contain the same members, but the characters occur in a different order.
A JSON object is an unordered collection of name/value pairs. An array is different: the order of its elements is part of the data.
So a program can parse the two objects and decide that they represent equivalent data. A raw SHA-256 operation cannot make that decision because it never parses them.
{"name":"Alice","age":25}
→ 45ca59b78c209d8bec5e7aa355d217c1a110c9105297bf641f8e090115bc0262
{"age":25,"name":"Alice"}
→ 0424dfbbc98dacd1e58bf68f2d8f37d388700a05160372e6e05ea91712f563deSame apparent data. Different representation. Different bytes. Different digest.
But duplicate keys are a much bigger problem
Now we have a different kind of problem. This time, it is not just the hash that disagrees with us; two parsers can disagree with each other.
{
"role": "user",
"role": "admin"
}What is the value of role?
There is no single interpretation you can safely assume across implementations. RFC 8259 says object names SHOULD be unique and warns that duplicate names lead to unpredictable behavior: many implementations keep the last value, while others reject the object or expose multiple values.
Now imagine that this JSON passes through two different systems. The first system validates it. The second system makes an authorization decision from it.
If those systems disagree about which value the JSON contains, the same request can effectively become two different requests depending on which component is looking at it.
That is no longer an interesting hashing quirk. It is a security boundary problem.
Case study: Apache APISIX and CVE-2022-25757
This is where the subject stops being a neat hashing trick and becomes a security issue. Apache APISIX was hit by exactly this kind of parser disagreement.
In CVE-2022-25757, APISIX's request-validation plugin could be bypassed when processing JSON containing duplicate keys. The affected setup involved APISIX using lua-cjson, which selected the last occurrence of a duplicate key, while an upstream application could use a JSON library that selected the first occurrence.
{
"string_payload": "bad",
"string_payload": "good"
}The dangerous part was the boundary between the two systems. APISIX could validate one interpretation of the request, then the upstream application could process another.
In the documented vulnerable configuration, the value used during validation could be different from the value ultimately consumed by the application.
APISIX fixed the vulnerable path by re-encoding the JSON it had validated and putting that representation back into the request body before forwarding it upstream. In other words, the application received the version that APISIX had actually checked.
The useful lesson is not 'watch out for APISIX.' It is to treat every parser boundary as a contract: if one component validates structured data and another consumes it, they need to agree on the same interpretation—or the first component's validation may not mean what you think it means.
The real problem is a disagreement at the boundary
The APISIX vulnerability makes the distinction much clearer.
There are two related but different problems.
The first explains why equivalent JSON documents can have different hashes.
The second explains why ambiguous JSON can become a security problem when multiple components process the same message.
Both problems come from the same underlying issue: the system has not made the boundary between data, representation, and interpretation precise enough.
So what should a cryptographic system actually hash?
That depends on what the hash is supposed to identify.
All three are legitimate operations. They simply answer different questions.
The mistake is assuming that a hash of raw JSON text automatically acts as a fingerprint of the abstract JSON data.
A real protocol: JWK thumbprints
The JSON Web Key ecosystem shows what it looks like when a protocol deliberately defines the bytes instead of leaving serialization to chance.
RFC 7638 defines a JWK Thumbprint: a hash value that can be used to identify or select the key represented by a JSON Web Key.
The important part is that the RFC does not say 'hash whatever JWK text you received.' It first defines a smaller, deterministic representation of the key, and only then hashes it.
It defines which members participate, how those members are ordered, how whitespace is handled, and how the resulting JSON is converted to UTF-8 bytes before hashing.
{
"kty": "RSA",
"n": "...",
"e": "AQAB",
"alg": "RS256",
"kid": "2011-04-29"
}For the RSA example in RFC 7638, only the required members kty, n, and e participate in the thumbprint. They are placed in lexicographic order.
{"e":"AQAB","kty":"RSA","n":"..."}Those exact UTF-8 bytes are then hashed. In the RFC's published RSA example, SHA-256 produces this thumbprint:
NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9XsThat is the pattern to remember: define the bytes first; hash them second.
Canonicalization is how you make those bytes deterministic
The JWK example points toward a broader idea: canonicalization. One important detail, though: RFC 7638 defines its own specific JWK thumbprint representation; it is not simply saying 'use RFC 8785.' Different protocols can define different canonical forms for their own purposes.
Canonicalization takes data that can have multiple valid representations and produces one representation according to a fixed set of rules.
RFC 8785, the JSON Canonicalization Scheme, was designed for exactly this kind of cryptographic use. It defines deterministic serialization of JSON primitives, deterministic property sorting, I-JSON constraints, and UTF-8 generation.
The point is simple: once the rules are fixed, two implementations can arrive at the same bytes even if the JSON they started with was formatted differently.
This means two systems can start with differently formatted JSON and still reach the same cryptographic input, provided they follow the same canonicalization rules.
Canonicalization is not the same thing as minification
It is tempting to summarize canonicalization as 'remove whitespace and sort the keys.' That is a decent first intuition. It is not the specification.
{"b":2,"a":1}{"a":1,"b":2}Both are already minified. A deterministic canonicalization scheme still has to decide which representation is the canonical one.
JCS recursively sorts object properties. Objects found inside arrays are also canonicalized, but the array elements themselves are not reordered because array order is meaningful.
["first","second"]That must not become this:
["second","first"]Changing array order would change the data, not merely its representation.
Numbers are part of the canonicalization problem too
Numbers look simple until different systems start serializing them.
{"price":4.50}{"price":4.5}Both represent the same numerical value, but they are different strings.
A canonicalization scheme therefore has to define how numbers are serialized rather than leaving that decision to whichever programming language happens to be running.
This becomes more important with large integers. JavaScript's ordinary Number type cannot exactly represent every integer beyond 2^53 - 1.
{
"userId": 9007199254740993
}If an application parses that value into an ordinary JavaScript Number and later serializes it, the exact original integer may not survive.
{
"userId": "9007199254740993"
}When the exact digits are an identifier rather than a quantity that needs arithmetic, representing the value as a string avoids asking a floating-point representation to preserve an integer it cannot represent exactly.
Why JSON.stringify() is not automatically a cryptographic protocol
If you work in JavaScript, JSON.stringify() is probably the first solution that comes to mind.
const user = {
name: "Alice",
age: 25
};
const json = JSON.stringify(user);For ordinary application code, there is nothing wrong with this. JSON.stringify() is useful precisely because it gives JavaScript a predictable way to turn a value into JSON text.
The trouble starts when that output is treated as a cross-language cryptographic protocol. Another runtime may make different serialization choices, and even JavaScript's property ordering rules are not the same thing as the ordering required by a canonicalization scheme.
A cryptographic protocol cannot leave those decisions implicit. It has to define the representation that gets authenticated or hashed.
RFC 8785 explicitly notes that ordinary JSON.stringify() output can preserve property creation or reception order, while canonicalization deliberately applies deterministic ordering for cryptographic purposes.
So this is not an argument against JSON.stringify(). Use it when your application needs ordinary JSON serialization. Just do not mistake a convenient serializer for a cryptographic interoperability standard. If a protocol specifies canonicalization or an exact signing input, follow that protocol.
What the two case studies have in common
APISIX and JWK thumbprints look like completely different subjects: one is a web-security bug, the other is a cryptographic identifier. They meet at the same uncomfortable question: what exactly does this JSON mean, and which bytes should another system trust?
In APISIX, two components could disagree about the value being validated. In a JWK thumbprint, the protocol avoids that ambiguity by defining exactly which members and bytes count.
But they expose the same underlying engineering problem: multiple systems need to agree on what a piece of structured data means and, when cryptography is involved, which exact bytes represent it.
The first can break validation. The second can break signatures, hashes, identifiers, or integrity checks.
The common solution is not to make JSON less useful. It is to make the boundaries around JSON precise.
There are three different things you might mean by 'the same JSON'
The phrase sounds simple, but it can hide three different questions.
A raw SHA-256 hash answers the first question if you hash the raw document.
A structural comparison answers the second.
A canonical cryptographic hash answers the third, provided the canonicalization procedure is explicitly defined and consistently implemented.
Try the experiment yourself
The easiest way to understand all of this is to reproduce the first experiment.
{"name":"Alice","age":25}Hash that exact text with SHA-256.
{
"name": "Alice",
"age": 25
}Hash the formatted version.
{"age":25,"name":"Alice"}Then hash the version with the keys reversed.
The three documents can represent the same object, but their raw byte sequences are different, so their raw SHA-256 digests are different.
You can reproduce the experiment with the Olivez Hash Generator & Checksum Checker. The point of the experiment is not that one hash is more correct than another. The point is to see exactly what happens when the bytes supplied to the hash function change.
And signatures solve the problem in their own way
There is another useful distinction here. Not every cryptographic system that carries JSON uses a general-purpose canonical JSON format.
JSON Web Signature (JWS), for example, defines an exact signing input instead. In the compact form, the signature is computed over the ASCII bytes of the base64url-encoded protected header, a period, and the base64url-encoded payload.
BASE64URL(UTF8(protected header))
.
BASE64URL(payload)That matters because the protected header's original JSON representation is part of what gets encoded. JWS is therefore not secretly doing RFC 8785 canonicalization for you. The protocol defines a different, exact byte-level construction for the thing being signed.
This is a useful rule of thumb: when cryptography is involved, do not ask only 'What JSON library should I use?' Ask 'What exact bytes does the protocol say I must authenticate?'
The bigger lesson is not really about JSON
JSON happens to make this problem easy to see because it is deliberately flexible about formatting and object member order.
The deeper problem is much broader than JSON. Software deals in things we think of as objects—users, payments, keys, permissions—but computers eventually exchange representations of those objects: bytes on a wire, in a file, or inside a message.
The more components a message passes through—libraries, runtimes, proxies, gateways, databases, services—the more expensive it becomes to leave those representation rules implicit.
The APISIX vulnerability shows what can happen when two components interpret the same JSON differently.
JWK thumbprints show the opposite approach: define exactly what data matters and exactly how it becomes bytes before hashing it.
JCS takes that idea and turns it into a general deterministic representation for JSON data used in cryptographic operations.
The takeaway
A JSON parser asks, 'What does this document represent?'
A hash function asks, 'What exact bytes did I receive?'
Both questions are valid. They just live at different layers.
Two JSON files can therefore mean the same thing and still have completely different hashes.
And when two systems disagree about what the JSON means, the consequences can be much more serious than a different digest. They can affect validation, authorization, signatures, identifiers, and other security decisions.
There is no need to pretend that JSON has one universal text form. When an application needs deterministic behavior, the protocol has to say what representation counts.
Sometimes that means hashing the exact file. Sometimes it means parsing and comparing the data. And when cryptography needs equivalent JSON to produce equivalent bytes, it can mean canonicalization.
Once you separate meaning from representation, the 'different hash' mystery is not really a mystery at all.
