JSON Diff
Compare two JSON objects and find differences instantly
Comparing two JSON documents by eye is unreliable once they are more than a few lines long: reordered keys and whitespace differences hide the changes that matter. This tool parses both sides, walks them recursively, and reports only real differences with a path for each one.
How the Comparison Works
Both inputs are parsed with JSON.parse. The top level of each must be an object; arrays and primitives at the root are rejected. The tool then takes the union of keys at each level and walks them.
- If a key is present on both sides and both values are plain objects, it recurses into that object rather than treating it as a leaf.
- Everything else — primitives, arrays,
null, and any key present on only one side — is compared as a leaf with deep equality. - Missing keys are tracked as
undefined: a key only on the left is removed, a key only on the right is added, and a key that exists on both with unequal values is changed. - Paths are joined with dots, so a nested key becomes
address.city. - Finally, all entries are sorted alphabetically by path, and four counts are produced.
Because arrays are never recursed into, the tool treats an entire array as one value. Two arrays are equal only if their lengths match and every element is deeply equal.
How to Use
- Paste the original JSON into the left editor. The page pre-fills a small sample so you can see the behavior immediately.
- Paste the modified JSON into the right editor.
- Click Compare. Nothing runs until you press the button.
- Read the results. Green means added, red means removed, yellow means changed, and gray means unchanged. Changed entries show the old value struck through followed by the new value.
Invalid JSON on either side produces a parse error naming that side, so a trailing comma or a missing quote is easy to locate.
A Worked Example: An API Response
Take the two sample objects the page starts with.
Left:
{
"name": "Alice",
"age": 30,
"email": "alice@example.com",
"address": { "city": "New York", "zip": "10001" }
}
Right:
{
"name": "Alice",
"age": 31,
"email": "alice@newdomain.com",
"address": { "city": "Brooklyn", "zip": "10001" },
"phone": "+1234567890"
}
The tool recurses into address and reports the following entries, sorted by path:
| Path | Type | Detail |
|---|---|---|
address.city | changed | New York → Brooklyn |
address.zip | unchanged | 10001 |
age | changed | 30 → 31 |
email | changed | alice@example.com → alice@newdomain.com |
name | unchanged | Alice |
phone | added | +1234567890 |
The summary reads 1 added, 0 removed, 3 changed, and 2 unchanged. Note that address itself is not listed as a change — only its changed child is, which keeps the output focused on what actually differs.
What It Flags vs. What It Skips
| Situation | Reported as |
|---|---|
| Key only on the right | added |
| Key only on the left | removed |
| Same key, different primitive value | changed |
| Same key, different type (object vs string) | changed, with both values shown |
| Same key, deeply equal values | unchanged |
| Array with a different length or element | changed (the whole array) |
| Array with identical elements in the same order | unchanged |
| Keys in a different order | no difference |
| Reindented input | no difference |
That last two rows are the reason to use a structural diff instead of a text diff for JSON: formatting churn disappears.
Why Not Just Use a Text Diff?
A line-based text diff compares characters, so it reports a change every time a key moves or indentation shifts. JSON has no meaningful key order, and pretty-printers change whitespace freely, so a text diff on two semantically identical documents can light up the entire file. This tool parses first and compares values, which removes that noise. It makes the opposite trade-off for arrays: a text diff can show which lines were inserted, while this tool reports the whole array as one changed value. Use the JSON diff when structure matters, and a text diff when the exact character edits matter.
When Arrays Are Not Enough
Because arrays are all-or-nothing, a one-element insertion at the start of a large array is reported as a single changed entry showing the entire old and new arrays. The tool will not tell you that items[0] was inserted or that items[3].id changed. For small arrays this is fine, and it avoids ambiguous alignment when elements have no stable identity. If you need element-level comparison, normalize the data first — for example, convert the array into an object keyed by id — and then diff the normalized form.
Reading the Output and Counts
The four counters include unchanged leaves, so they sum to the total number of compared leaves, not to the number of differences. A diff with zero differences shows a short “no differences” message and still displays all four counters. Because unchanged entries are included in the list, a large object can produce a long result even when only one value moved; scan the colored badges for added, removed, and changed if you only care about what moved. Everything is processed locally in the browser, so API responses and configuration files are never transmitted.
Related Tools
- JSON Formatter — Validate, format, and beautify raw JSON strings into readable, indented structures.
- Diff Checker — Compare two blocks of plain text side by side with highlighted differences.
- JSON to CSV — Convert JSON arrays into comma-separated values for spreadsheet analysis.
- YAML to JSON Converter — Convert between YAML and JSON formats.
Frequently Asked Questions
Why does the tool require the top level to be an object?
The diff is built around named keys and dot-notation paths such as `address.city`, so it expects a JSON object at the root of each input. A bare array, a string, a number, or `null` at the top level is rejected with a specific error. Wrap arrays in an object, for example `{ "items": [...] }`, if you need to compare them.
How are array elements compared?
Arrays are treated as single leaf values and compared with deep equality. If the length differs, or any element differs at any depth, the whole array is reported as changed with the old and new values shown side by side. The tool never reports individual array indices such as `items[2]`; element-level diffing is not implemented.
In what order are the differences listed?
Paths are sorted alphabetically with a locale-aware string comparison after the recursive walk finishes. That makes the output deterministic, so the same inputs always produce the same ordering. It also means `address.city` appears before `age`, because the comparison is character by character rather than by nesting depth.
What counts toward the unchanged count?
Every leaf that exists in both inputs with deeply equal values is counted as unchanged, including nested keys. When both sides are objects, the tool recurses and the parent path is not itself counted; only the leaves are. If you added a key, removed a key, or changed a value, it is not counted as unchanged.
How does it handle null versus a missing key?
A key that is explicitly `null` is present and is compared as a value; a key that is absent is undefined. So changing a field from `null` to a string is reported as changed, while adding a field that happens to be `null` is reported as added. This distinction matters when an API signals 'no value' with null rather than omitting the field.
Does key order or whitespace affect the result?
No. Both inputs are parsed with JSON.parse and compared by value, so reordering keys or reformatting indentation produces no differences. Only actual value, type, and presence changes are reported, which is what makes the tool useful for noisy diffs in version control.
Reviewed by the HandyTools Hub team · Last updated