json json patch api testing diff

Compare Two JSON Responses and Generate a JSON Patch

Why line-based diffs fail on JSON, how to compare arrays by position vs by key, why big integers need a lossless parser, and how to turn the difference into an RFC 6902 JSON Patch.

· Mohammed Aquib Ansari

I compare JSON more often than I’d like to admit: the response from staging against production, an API before and after a refactor, a config file that “nobody changed”. For a long time I pasted both into a text diff tool and squinted. That works until the serializer reorders keys, or an array gets a new item at the top, and suddenly every line is red.

JSON has structure, so the comparison should use it. Here’s what I’ve learned doing that, using the JSON diff tool.

A real example

Two versions of an order from an API:

{
  "id": 1042,
  "status": "pending",
  "items": [
    { "sku": "KB-104", "qty": 1 },
    { "sku": "MS-220", "qty": 2 }
  ],
  "updatedAt": "2026-10-01T09:00:00Z"
}
{
  "id": 1042,
  "status": "paid",
  "items": [
    { "sku": "MS-220", "qty": 2 },
    { "sku": "KB-104", "qty": 1 },
    { "sku": "CB-010", "qty": 3 }
  ],
  "updatedAt": "2026-10-01T09:05:12Z"
}

What actually changed? The status, and one new line item. But compare the arrays by position and you get seven changes: /items/0/sku, /items/0/qty, /items/1/sku, /items/1/qty, the new /items/2, plus status and updatedAt. The server just returned the items in a different order.

Arrays: by position or by key

This is the most important setting in any JSON diff, and most tools hide it.

By position compares item 0 with item 0. That’s right when order means something: steps in a pipeline, points in a polyline, a sorted leaderboard.

By key matches items by an ID field. For the order above, matching on sku and ignoring updatedAt gives exactly two changes:

Changed  /status    "pending" → "paid"
Added    /items/2   {"sku":"CB-010","qty":3}

That’s the diff I actually wanted. The tool always shows which strategy is active, because the two results can look completely different for the same input.

Ignore the noise

Timestamps, ETags, request IDs and generated GUIDs change on every call. Put them in the ignore list — by key name (updatedAt) to ignore them anywhere, or by JSON Pointer pattern (/items/*/etag) for a specific place. I keep a short list per API I test.

Big numbers are where most tools lie

This one bit me with a payments API that used 64-bit IDs:

{"id": 9007199254740993}
{"id": 9007199254740992}

Those are different numbers. But JSON.parse in JavaScript turns both into the same double, because doubles only hold integers exactly up to 2^53 (9,007,199,254,740,991). Any browser-based diff that uses JSON.parse will report them as equal. The JSON diff tool parses numbers losslessly and compares their decimal text, so it reports the change.

The flip side: 1 and 1.0 are the same value, and the tool treats them as equal unless you tick “1 and 1.0 are different” — useful when you’re checking a serializer’s exact output.

Type changes are their own category

A quantity that went from 2 to "2" looks almost identical in a text diff. It is not the same to a typed client: a C# int property will fail to deserialize, and in JavaScript "2" + 1 is "21". The tool reports these as type changed (number → string) rather than a plain change, so they stand out.

From diff to JSON Patch

Sometimes I don’t just want to see the difference — I want to send it. RFC 6902 JSON Patch describes changes as a list of operations, and HTTP APIs accept it with Content-Type: application/json-patch+json. ASP.NET Core supports it through Microsoft.AspNetCore.JsonPatch; in JavaScript, fast-json-patch applies it.

For the order above (ignoring updatedAt), the tool generates:

[
  { "op": "replace", "path": "/status", "value": "paid" },
  { "op": "replace", "path": "/items/0/sku", "value": "MS-220" },
  { "op": "replace", "path": "/items/0/qty", "value": 2 },
  { "op": "replace", "path": "/items/1/sku", "value": "KB-104" },
  { "op": "replace", "path": "/items/1/qty", "value": 1 },
  { "op": "add", "path": "/items/2", "value": {"sku":"CB-010","qty":3} }
]

Notice the patch uses positions even though I matched by key for reading. That’s deliberate: JSON Patch paths are positional, so a patch built from key matching wouldn’t apply with standard libraries. The patch is tested by applying generated patches to hundreds of random document pairs and checking the result equals the right-hand side.

Two details that matter if you build patches by hand: / inside a key is written as ~1 and ~ as ~0 ({"a/b": 1} is at /a~1b), and array removals should go from the highest index down, or the earlier removals shift the later ones.

When I still use a text diff

When formatting itself is the question — whitespace in a checked-in file, a trailing comma, the exact bytes a webhook signed — structure is the wrong lens. Then I use the text diff. For everything about data, the JSON diff is faster and far less noisy. And if the input is a mess to begin with, run it through the JSON formatter first to find the syntax error.