JSON Diff
Compare two JSON documents structurally, not as text: key order and indentation stop counting as differences. Matches arrays by id and flags type changes.
Options
| Path | Original | Compared | |
|---|---|---|---|
| ≠ | contacto.email | "[email protected]" | "[email protected]" |
| − | contacto.tel | "2221234567" | — |
| ≠ | productos[id=1].precio | 18 | 20 |
| + | productos[id=0] | — | {"id":0,"nombre":"Cuernito","precio":22} |
| T | sucursalesnumber → string | 3 | "3" |
What this tool does
It compares two JSON documents structurally, not as text. It walks both trees at once and reports every difference with its path (contact.email, products[id=2].price), classified into four kinds: added, removed, changed and type change.
The practical consequence is that key order and indentation stop existing as differences, and what stays on screen is only what actually changed.
Everything happens in your browser: you can paste API responses containing real data without it leaving your machine.
How to use it
- Paste the original JSON on the left and the one you want to compare on the right.
- If either has a syntax error, you get the line, the column and what is missing.
- Read the difference table. Tick also show what did not change to see the whole document.
Why a text diff does not work for JSON
This is the reason this page exists. A line diff — the kind Git, editors and this site’s own text comparer use — is excellent for code, and counterproductive for JSON:
These two documents are identical:
{ "name": "Ana", "age": 30 }
{ "age": 30, "name": "Ana" }
In JSON an object is an unordered set of key-value pairs. The specification says so and every parser honours it. But a text diff marks both whole lines as changed, because the characters sit somewhere else.
And the other way round: reformatting a file with jq or with your editor’s “format” button changes no data at all, and produces a diff of hundreds of lines.
The result is that as soon as JSON comes from two different sources — two environments, two API versions, an export and an import — the text diff is useless: everything shows as changed and you cannot tell what matters.
The array problem, and how it is solved here
This is the detail that separates a useful comparer from one that is not.
An array does have order, so the natural thing is to compare element 0 with 0, 1 with 1, and so on. That works until somebody inserts an element at the start:
// before
[ {"id": 1, "name": "Concha"}, {"id": 2, "name": "Bolillo"} ]
// after
[ {"id": 0, "name": "Cuernito"}, {"id": 1, "name": "Concha"}, {"id": 2, "name": "Bolillo"} ]
Comparing by position: element 0 “changed” from Concha to Cuernito, element 1 “changed” from Bolillo to Concha, and element 2 is new. Three differences, and none of them is true. All that happened was one insertion.
The fix is to match by identifier instead of by position. The tool automatically looks for a stable key among id, _id, uuid, key, slug, code, sku, email and name, and only uses it if two conditions hold:
- it is present in every element of both arrays,
- and its values are unique within each array.
If there are duplicates or it is missing anywhere, matching by that key would lie, so it falls back to positional comparison. When it is used you are told which key was chosen, and paths become products[id=0] rather than products[2] — which is also a path that still means the same thing tomorrow, when the order changes again.
You can switch it off in the options to see the contrast with your own data.
The type change, which is kept separate
When the same path holds 1 in one document and "1" in the other, it is not a value change: it is a type change, and it gets its own category and its own colour.
The distinction matters because it is rarely intentional. It almost always means one of three things:
- An API started returning numbers as strings, typically after changing ORM or serializer.
- A field went from
nullto0, or the other way, and the consuming client distinguishes “no data” from “the data is zero”. - A half-finished migration left some records in one format and some in another.
These are differences that break a client even though the value “looks the same” on screen. A text diff cannot tell them apart from any other change; here they stand out.
When this actually gets used
- Comparing an API response across two environments (local versus production, or before and after a deploy) to find the field that changed by accident.
- Reviewing a data migration: the dump from before against the one from after.
- Debugging an API contract when the client fails and the response “looks the same”.
- Comparing two configuration files, two
package.jsonfiles, two translation exports. - Auditing what a third-party integration changed between two runs.
The three syntax errors that show up most
They are things JavaScript accepts and JSON does not, and the tool names them explicitly instead of echoing the browser’s cryptic message:
| Written | Problem |
|---|---|
{"a": 1,} | Trailing comma before the closing token. Valid in JS, forbidden in JSON |
{'a': 1} | Single quotes. JSON requires double |
{a: 1} | Unquoted key. In JS it is a valid object; in JSON it is not |
The line and column come from a purpose-built analyzer, not from the JSON.parse message. The reason is concrete: every engine formats that message differently, and in current Chrome and Node two formats coexist, only one of which includes the position. A purpose-built analyzer returns the same thing in any browser, and it can also explain the error rather than describe it.
Frequently asked questions
How is this different from comparing two JSON files with a text diff?
A text diff compares lines, and in JSON that produces constant noise. Two documents with the same keys in a different order are exactly the same object, but a line diff marks them entirely as changed; and reindenting a file, which changes no data at all, generates hundreds of false differences. This tool compares values by path: key order and formatting do not count, and the only thing reported is what actually changed.
Why does inserting one array element mark everything as changed?
Because most comparers match array elements by position. If you insert one at the start, what was at index 0 moves to 1, index 1 moves to 2, and they all show up as modified even though you touched none of them. This tool detects whether the elements carry a stable identifier — `id`, `_id`, `uuid`, `slug`, `code`… — and matches them by it: then an insertion is reported as a single insertion. You can switch it off in the options to see the difference.
What is a “type change” and why is it flagged separately?
It is when the same path holds values of different natures in the two documents: `1` versus `"1"`, `null` versus `0`, an array versus an object. It is flagged separately because it is rarely a data change: it usually points to a serialization bug, an API that started returning numbers as strings, or a half-finished migration. It is the kind of difference that breaks a client even though the value "looks the same".
Are my JSON documents sent to any server?
No. All the analysis, parsing and comparison happen in your browser with JavaScript. The documents are not uploaded, not stored and not logged. You can compare API responses with real data, configuration files or database dumps without them leaving your machine.
Why does my JSON error out when it looks correct to me?
The three most common reasons are things JavaScript allows and JSON does not: the trailing comma before a closing token (`{"a": 1,}`), single quotes instead of double (`{'a': 1}`) and unquoted keys (`{a: 1}`). This tool detects them and says so in those words, plus it gives you the exact line and column. That position comes from its own analyzer, not from the browser message, because every engine formats it differently and some do not include a position at all.
Can I ignore differences I do not care about?
Yes, with three options. Ignore case compares strings without distinguishing capitalisation. Ignore surrounding whitespace trims the start and end of each string, useful when a system pads values on export. And treating `1` and `"1"` as equal helps against APIs that return every value as text. That last one does not hide the type change, it just stops it being flagged as a value change too.
Reviews & ratings
No reviews yet. Be the first to leave one!
Related tools
Others from the catalogue that pair well with this one.