A worked example: an API that changed under you
Here is the kind of comparison the tool is for. The first document is a page of payouts from a payments API, as your integration was written against it. The second is the same endpoint after the provider shipped a new version:
// Original
{
"object": "list",
"data": [
{ "id": "po_1001", "amount": 12050, "currency": "eur", "status": "paid",
"arrival_date": "2026-09-26", "method": "standard", "failure_code": null },
{ "id": "po_1002", "amount": 8000, "currency": "eur", "status": "in_transit",
"arrival_date": "2026-09-30", "method": "instant", "failure_code": null }
],
"has_more": false
}
// Modified
{
"object": "list",
"url": "/v2/payouts",
"data": [
{ "id": "po_1003", "amount": "45.00", "currency": "eur", "status": "pending",
"arrival_date": null, "destination": { "type": "bank_account", "last4": "6789" } },
{ "id": "po_1001", "amount": "120.50", "currency": "eur", "status": "paid",
"arrival_date": "2026-09-26", "destination": { "type": "bank_account", "last4": "6789" },
"failure_code": null },
{ "id": "po_1002", "amount": "80.00", "currency": "eur", "status": "paid",
"arrival_date": "2026-09-30", "destination": { "type": "card", "last4": "4242" },
"failure_code": null }
],
"has_more": true
}
The Structure view: what broke
Most of the time the first question is not "which values differ" but "will my code still work". The Structure view answers it by comparing the shape of the two documents - every field path, the types seen there, and whether it is always present - and ignoring the values entirely:
| Field | Change | Detail |
|---|---|---|
data[].amount | Type changed (breaking) | number → string |
data[].arrival_date | Became nullable (breaking) | string → string | null |
data[].failure_code | Became optional (breaking) | missing from some elements |
data[].method | Field removed (breaking) | string |
data[].destination | Field added | object |
url | Field added | string |
Four breaking changes, and each maps to a specific failure. amount switched from integer cents (12050) to a decimal string ("120.50"), so arithmetic on it now concatenates or fails to parse. arrival_date can be null for pending payouts, so date parsing needs a guard. failure_code is sometimes absent rather than null - the difference the null, missing, and unknown fields comparison shows matters to Jackson, Pydantic, and Zod. And method is gone. The two added fields are safe: code that does not know about them simply does not read them.
The rules are deliberately simple: a removed field, a changed type, a field that can newly be null, and a field that is newly missing from some records are breaking. An added field, or one that is no longer null or no longer missing, is safe. Because the shape comes from the samples you paste, the more records each side contains, the more accurate the verdict.
The Changes view: what differs
The Changes view lists every value that was added, removed, or changed - ten here:
~ data[id="po_1001"].amount 12050 → "120.50"
- data[id="po_1001"].method "standard"
+ data[id="po_1001"].destination {"type":"bank_account","last4":"6789"}
~ data[id="po_1002"].amount 8000 → "80.00"
~ data[id="po_1002"].status "in_transit" → "paid"
- data[id="po_1002"].method "instant"
+ data[id="po_1002"].destination {"type":"card","last4":"4242"}
+ data[id="po_1003"] {"id":"po_1003","amount":"45.00",...}
~ has_more false → true
+ url "/v2/payouts"
Look at the paths: data[id="po_1001"], not data[0]. The new payout po_1003 arrived at the top of the list, which pushed the other two down a position. Compared position by position, data[0] would be po_1001 against po_1003 - and every field of both records would be reported as changed. Matched by id, the tool sees one new record and a few real changes to the existing ones.
Matching array items by ID
When every element of an array, on both sides, is an object with the same identifying field holding unique values, items are paired by that field instead of by position. The tool checks id, _id, uuid, guid, key, code, sku, slug, and name, in that order, uses the first that qualifies, and tells you which it picked. If no field qualifies - IDs repeat, or some items lack one - it falls back to position for that array.
For the example above the difference is large: 10 changes matched by id, against 16 compared by position, where the new record's fields are reported as changes to po_1001 and every record after it shifts by one. Switch Match array items to By position when the order itself is what you care about - a ranked list, or a sequence of steps.
Why a text diff gets JSON wrong
JSON is text, so it is tempting to reach for diff or your editor's compare view. The trouble is that text diffs see formatting, and JSON's meaning ignores it. I measured both failure modes on the example files:
- Same data, different formatting. Re-serialising the original with sorted keys and a 4-space indent - which is what a different JSON library on the server might do - changes nothing about the data.
diff -ureports 28 changed lines. JSON Diff reports 0 changes. - Real changes, normalised first. The usual command-line fix is to sort keys with jq before diffing:
diff <(jq -S . a.json) <(jq -S . b.json). That removes the key-order noise, but it still compares arrays line by line, so with the new record at the top it prints 32 lines pairingpo_1003's fields againstpo_1001's. JSON Diff reports the 10 changes above, each against the right record.
The jq recipe is still the right tool in a script or a CI job, where you want a plain exit code. For reading the differences yourself, a structural diff is faster and harder to misread.
Numbers are compared exactly
Like the JSON formatter, the diff never lets JSON.parse near your numbers. JavaScript would round 1839274619283746817 and 1839274619283746818 to the same value, so a diff built on JSON.parse would say two different IDs are equal. This tool compares the digits as written, so it reports the change.
The same exactness means 12.50 and 12.5 are reported as different by default, because the documents are. When you only care about numeric value - comparing output from two JSON libraries, for instance - tick Treat 12.50 and 12.5 as equal. That comparison is done on the decimal digits too, so it never makes two different large integers equal.
If a document contains the same key twice, the tool uses the last value, as JSON.parse does, and tells you where the duplicate is - two layers of a system can disagree about which value is real.
JSON Patch output
The JSON Patch view expresses the differences as an RFC 6902 patch: a list of add, remove, and replace operations that turns the original into the modified document.
[
{ "op": "add", "path": "/url", "value": "/v2/payouts" },
{ "op": "remove", "path": "/data/0/method" },
...
]
A patch addresses array elements by position, as the standard requires, so this view always compares arrays by position regardless of the matching setting. The tool's test suite applies generated patches to thousands of random document pairs and checks that each one reproduces the modified document exactly.
What it does not do
- No text-level diff inside strings. A changed string is shown as the old and new value, not as a character-by-character highlight.
- No three-way merge. It compares two documents; it does not reconcile two sets of changes against a common base.
- No
moveoperations in the patch. A reordered array producesreplaceoperations, which are correct but longer than a patch with moves would be. - Not built for very large files. Both documents are held in memory in your browser tab; file uploads are capped at 5 MB each.
Other tools
- Run cURL - fetch the two responses you want to compare straight from their APIs
- JSON Formatter - pretty-print both documents before reading them side by side
- JSON Flattener - one line per field path, for a quick inventory of either side
- JSON to TypeScript - generate the type for the new shape once you have seen what changed
Further reading
- Null, missing, or unknown - how Jackson, Pydantic, and Zod react to exactly these breaking changes
- Common JSON structures in REST APIs
- JSON Schema: a practical guide - writing the contract down, so changes are caught before they ship
Written and maintained by Ashish Singh · Last updated · Changelog · Found a problem with this page? Tell me.