It's Friday afternoon, the frontend is broken, and the backend team swears they "didn't change anything". You have yesterday's API response and today's, both a few hundred lines long. Squinting at them side by side is a fine way to lose an hour.
A JSON diff does the squinting for you. It lists every value that was added, removed or changed, with the exact path to each one. This guide covers when that helps, how diffing works under the hood, and how to read the results.
Why compare JSON objects
Debugging API responses
When an endpoint starts returning odd data, compare today's response with one you know was good. The diff shows which fields changed, which were added and which quietly disappeared.
{
"user": {
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"role": "admin"
}
}{
"user": {
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"roles": ["admin"]
}
}The diff shows that role (a string) was removed and roles (an array) was added. So something did change: users can now have more than one role, and the frontend needs to catch up.
Catching configuration drift
Files such as package.json, tsconfig.json, deployment manifests and feature flag configs start out identical across development, staging and production. Then someone hotfixes production at 2 a.m. and they never quite match again. Diffing them catches missing keys and surprise overrides before they cause an incident.
Validating data migrations
After a migration, compare the transformed JSON with a hand-written expected output to confirm no fields were lost and no types changed by accident.
Reviewing test snapshots
When a JSON snapshot test fails, the diff between the old and new snapshot tells you whether to update the snapshot or fix a regression.
Shallow vs deep comparison
Shallow comparison
There are two ways to compare objects. A shallow comparison checks only the top-level keys. Nested objects are compared by reference (is this the same object in memory?) rather than by content, so it can tell you two objects differ, but not where.
function shallowEqual(obj1, obj2) {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
// Nested objects are compared by reference only
if (obj1[key] !== obj2[key]) return false;
}
return true;
}
const a = { name: "Alice", settings: { theme: "dark" } };
const b = { name: "Alice", settings: { theme: "light" } };
shallowEqual(a, b); // false, because the two settings objects are
// different references. It does not say that theme changed.Deep comparison
A deep comparison walks into every nested object and array on both sides and compares the actual values: strings, numbers, booleans and nulls. The result is a full list of differences, each with its path. This is how JSON diff tools work.
function deepDiff(obj1, obj2, path = "") {
const diffs = [];
const allKeys = new Set([
...Object.keys(obj1 || {}),
...Object.keys(obj2 || {}),
]);
for (const key of allKeys) {
const currentPath = path ? path + "." + key : key;
const val1 = obj1?.[key];
const val2 = obj2?.[key];
if (val1 === undefined) {
diffs.push({ type: "added", path: currentPath, value: val2 });
} else if (val2 === undefined) {
diffs.push({ type: "removed", path: currentPath, value: val1 });
} else if (
typeof val1 === "object" && typeof val2 === "object" &&
val1 !== null && val2 !== null
) {
diffs.push(...deepDiff(val1, val2, currentPath));
} else if (val1 !== val2) {
diffs.push({ type: "changed", path: currentPath, oldValue: val1, newValue: val2 });
}
}
return diffs;
}Reading diff results
Every difference is one of three types. The examples below use the same notation as the PulpMiner JSON diff tool: + for added, - for removed, ~ for changed, and paths that start at root (the top of the document).
Additions
A key or array element exists in the modified JSON but not in the original, for example a new field in an API response or a new config option.
{
"database": {
"host": "localhost",
"port": 5432
}
}{
"database": {
"host": "localhost",
"port": 5432,
"ssl": true
}
}+ root.database.ssl: trueRemovals
A key exists in the original but not in the modified JSON. Removals deserve the most attention, because any code that still reads the missing field will get undefined and may break at runtime.
- root.database.password: "s3cret"Changes
A key exists in both but its value differs. That covers a new value, a different type (string to number) and a different structure (string to array).
~ root.database.port: 5432 => 3306
~ root.user.role: "admin" => ["admin"]How arrays are compared
Objects line up neatly by key. Arrays only have positions, which makes them the awkward part of any diff. The PulpMiner tool compares arrays position by position: item 0 with item 0, item 1 with item 1, and so on. That's predictable, but insert one item at the front and everything after it shifts along:
{ "tags": ["frontend", "react", "typescript"] }{ "tags": ["javascript", "frontend", "react", "typescript"] }~ root.tags[0]: "frontend" => "javascript"
~ root.tags[1]: "react" => "frontend"
~ root.tags[2]: "typescript" => "react"
+ root.tags[3]: "typescript"Four entries, for what was really one new tag. Some libraries use a longest common subsequence (LCS) algorithm, which finds the items both arrays share and can report this as a single insertion. With a position-based tool, sort arrays whose order doesn't matter before comparing, so a reshuffle doesn't look like a rewrite.
Example: comparing two API versions
Here's a realistic one. Your payment provider has shipped version 2 of its API, and the changelog says "minor improvements". You fetch the same transaction from both versions:
{
"transaction_id": "txn_abc123",
"amount": 29.99,
"currency": "USD",
"status": "completed",
"customer": {
"id": "cust_001",
"email": "buyer@example.com"
},
"created_at": "2026-01-15T10:30:00Z"
}{
"id": "txn_abc123",
"amount": {
"value": 2999,
"currency": "USD"
},
"status": "succeeded",
"customer": {
"id": "cust_001",
"email": "buyer@example.com",
"name": "Jane Doe"
},
"created_at": "2026-01-15T10:30:00Z",
"metadata": {}
}The diff tells a less relaxed story:
transaction_idwas removed andidwas added (a rename).amountchanged from a number to an object, with the value now in cents.- The top-level
currencywas removed; it now lives atamount.currency. statuschanged from"completed"to"succeeded".customer.nameandmetadatawere added.
Every one of those needs a change in your integration. Better to find out now than from a support ticket.
How to use the JSON diff tool
- Open the JSON diff tool and paste the old version into Original JSON and the new one into Modified JSON.
- Select Compare. If either input is invalid JSON, the tool tells you which one and shows the parse error.
- Read the summary counts (added, removed, modified, unchanged) and the list of entries, each with its full path.
- Select Diffs only to hide unchanged values, and Copy to copy the result as text using the
+,-and~notation.
The comparison runs in your browser, so your JSON isn't uploaded.
Comparing JSON in code
For tests, CI pipelines and scripts, you'll want a diff library rather than a browser tab.
JavaScript and TypeScript
import { diff } from "deep-diff";
const left = { name: "Alice", age: 30, hobbies: ["reading"] };
const right = { name: "Alice", age: 31, hobbies: ["reading", "hiking"] };
const differences = diff(left, right);
// [
// { kind: "E", path: ["age"], lhs: 30, rhs: 31 },
// { kind: "A", path: ["hobbies"], index: 1,
// item: { kind: "N", rhs: "hiking" } }
// ]
// E = edited, A = array change, N = newPython
from deepdiff import DeepDiff
left = {"name": "Alice", "age": 30, "hobbies": ["reading"]}
right = {"name": "Alice", "age": 31, "hobbies": ["reading", "hiking"]}
print(DeepDiff(left, right))
# {'values_changed': {"root['age']": {'new_value': 31, 'old_value': 30}},
# 'iterable_item_added': {"root['hobbies'][1]": 'hiking'}}Tips for useful JSON diffs
- Don't worry about formatting or key order. A structural diff parses both inputs, so minified JSON and reordered keys don't produce false differences. A plain text diff is different: there you should pretty-print and sort keys first.
- Remove volatile fields. Timestamps, request IDs and random tokens change on every request. Strip them before comparing, or the real change will hide among 40 lines of new timestamps.
- Sort unordered arrays. If the order of a list such as tags doesn't matter, sort it on both sides first.
- Record expected differences. During an API migration, keep a list of intended changes so the team can tell them apart from regressions.
