A deploy changed something and you have two responses: staging and production, or before and after. A text diff of the raw bodies usually produces a wall of red and green, because two JSON documents can carry identical data and still differ on almost every line. What you want is a list of the values that actually changed.
The structural JSON diff parses both documents and compares them value by value, path by path, in your browser. Paste the original on the left and the modified on the right. If either side doesn't parse, it reports the line and column of the error, just as the JSON formatter does.
Why a text diff misleads
Here are two responses for the same user, captured a few seconds apart:
{
"requestId": "req_8f2a",
"generatedAt": "2026-10-24T09:12:03Z",
"user": { "id": 42, "plan": "pro", "seats": 5 },
"features": ["export", "sso"]
}
{
"user": { "plan": "pro", "id": "42", "seats": 5, "trialEndsAt": null },
"features": ["sso", "export"],
"generatedAt": "2026-10-24T09:12:07Z",
"requestId": "req_c41d"
}
A line diff marks every line as changed. Some of that is real, but much of it is key order, which carries no meaning: RFC 8259 defines an object as an unordered collection of name/value pairs. Whitespace is the other usual source of noise, when one side is minified and the other pretty-printed. You can tame a text diff by normalising both sides first, with Sort keys and the same indent in the formatter or with jq -S . response.json, but arrays and volatile fields will still get in the way.
What a structural diff reports
The same two documents in the JSON diff, List view, showing only changes:
~ $.requestId: "req_8f2a" → "req_c41d"
~ $.generatedAt: "2026-10-24T09:12:03Z" → "2026-10-24T09:12:07Z"
~ $.user.id: 42 → "42"
+ $.user.trialEndsAt: null
~ $.features[0]: "export" → "sso"
~ $.features[1]: "sso" → "export"
Key order has dropped out. What is left needs triage:
$.user.idchanged type, from the number 42 to the string "42". This is the line that breaks clients:42 === "42"is false, and a TypeScript type that saysnumberis now wrong. The diff reports any type change as a change, even when the printed values look alike.trialEndsAtwas added with the valuenull. A key that is present withnullis not the same as a missing key, and the diff keeps them apart. Ifnulllater becomes a date, that is reported as a change, not an addition.requestIdandgeneratedAtare noise. They differ on every request.- The
featuresarray was reordered. Whether that matters depends on the API.
The Tree view shows the same result nested, with + added, − removed and ~ changed markers. The Show filter switches between everything, only changes, only additions and only removals, and the status bar counts each kind.
Arrays: by index or by ID
Arrays are ordered in JSON, so by default the diff compares them position by position. That is right for ranked results or time series and wrong for collections of records whose order is incidental. Take an order's line items:
Original: {"items": [{"id": "a1", "qty": 1}, {"id": "b2", "qty": 3}]}
Modified: {"items": [{"id": "b2", "qty": 4}, {"id": "a1", "qty": 1}, {"id": "c3", "qty": 2}]}
By index, every field of the first two items looks changed and the third is added: five entries for what is really one quantity change and one new item. Set Arrays to By ID field with the field name id, and the diff matches items by that field instead:
~ $.items[id=b2].qty: 3 → 4
+ $.items[id=c3]: {"id":"c3","qty":2}
Items without the ID field are still compared, matched by their order among the other ID-less items. ID matching doesn't help with arrays of plain strings like features. If their order doesn't matter, sort them on both sides before comparing, for example with jq '.features |= sort'.
Strip the noise before you compare
Request IDs, trace IDs, timestamps, ETags and pagination cursors differ on every call. The DevPrix diff has no ignore list; it reports everything it sees, so remove volatile fields first. With jq, at any depth:
jq 'walk(if type == "object" then del(.requestId, .generatedAt) else . end)' response.json
Or in a browser console or Node, with a JSON.stringify replacer that drops the named keys wherever they appear:
const NOISE = new Set(["requestId", "generatedAt"]);
const clean = (text) =>
JSON.stringify(JSON.parse(text), (key, value) => (NOISE.has(key) ? undefined : value), 2);
You can also compare less. If only the user object matters, use the JSONPath evaluator to pull $.user out of each response and diff just those. Don't strip a timestamp that is the actual bug, though: a value off by a factor of exactly 1000 is a seconds-versus-milliseconds mix-up, not noise.
Export the difference as a JSON Patch
The JSON Patch view turns the diff into an RFC 6902 document that transforms the original into the modified. For the first example, condensed to one operation per line:
[
{ "op": "replace", "path": "/requestId", "value": "req_c41d" },
{ "op": "replace", "path": "/generatedAt", "value": "2026-10-24T09:12:07Z" },
{ "op": "replace", "path": "/user/id", "value": "42" },
{ "op": "add", "path": "/user/trialEndsAt", "value": null },
{ "op": "replace", "path": "/features/0", "value": "sso" },
{ "op": "replace", "path": "/features/1", "value": "export" }
]
Paths are JSON Pointers (RFC 6901), in which ~ is written ~0 and / is written ~1. The patch uses only add, remove and replace, and always addresses array items by index, even when the tree matches them by ID, so it applies cleanly to the original document. A reorder therefore shows up as replacements rather than moves. Copy puts the patch on your clipboard and Download saves it as diff.patch.json. In a bug report it is far more precise than "the user object looks different", and a JSON Patch library can apply it to a test fixture.
A repeatable routine
- Capture both responses the same way: same endpoint, query parameters, user, headers and API version.
- Make sure both parse. An HTML error page on one side is a finding in itself.
- Strip volatile fields, or extract only the subtree you care about.
- Choose how arrays are compared: by index for ordered data, by ID for records.
- Filter to changes and read type changes and removals first; they are the ones that break clients.
- Attach the JSON Patch to the ticket.
Next steps
Paste your two responses into the JSON diff and set the filter to Only changes. If it turns up a type change like 42 becoming "42", check your generated types too; converting JSON to TypeScript safely explains why one sample is never enough.