# Diff two API responses without the noise

You changed a serializer, bumped a dependency, or moved an endpoint to a new service. Now you want to know one thing: **did the response change?**

The usual move is to save both responses and run `diff before.json after.json`. That works until it doesn't.

## Why text diffs lie about JSON

Take this response before and after a refactor:

```json
{ "id": 42, "name": "Alice", "role": "admin",
  "tags": ["beta", "staff"],
  "plan": { "tier": "pro", "seats": 5 } }
```

```json
{ "id": 42, "name": "Alice", "roles": ["admin", "editor"],
  "tags": ["staff", "beta"],
  "plan": { "tier": "pro", "seats": "5" } }
```

A line diff has three problems here:

1. **Formatting noise.** If one side is minified or the keys come out in a different order, every line "changed".
2. **Reordered arrays look like edits.** `["beta", "staff"]` vs `["staff", "beta"]` is usually the same set of tags, but it shows up as two changed lines.
3. **Type changes are nearly invisible.** `5` vs `"5"` is two characters. In a 400-line response you will scroll right past it, and it's the one that breaks clients.

## Diff the structure, not the text

A structural diff parses both sides and compares values by path. Running the two responses above through one gives:

| Path | Before | After | Change |
| --- | --- | --- | --- |
| `$.role` | `"admin"` | (missing) | removed |
| `$.roles` | (missing) | `["admin","editor"]` | added |
| `$.tags[0]` | `"beta"` | `"staff"` | changed |
| `$.tags[1]` | `"staff"` | `"beta"` | changed |
| `$.plan.seats` | `5` | `"5"` | changed |

Whitespace and key order are gone from the picture. But the tags rows are still noise.

## Turn on order-insensitive arrays

Compare arrays as sets instead of by index and the same diff becomes:

| Path | Before | After | Change |
| --- | --- | --- | --- |
| `$.role` | `"admin"` | (missing) | removed |
| `$.roles` | (missing) | `["admin","editor"]` | added |
| `$.plan.seats` | `5` | `"5"` | changed |

Three rows, and every one is real:

- `role` was renamed to `roles` and became an array. Breaking change for every client reading `role`.
- `seats` went from number to string. Breaking change for anything doing math on it.

For arrays of objects (say, a list of line items), matching elements by their `id` instead of by position gives the same benefit: inserting one item at the top doesn't mark every following item as changed.

## Doing it in the browser

I built this into [Formaty's JSON Diff](https://formaty.dev/json-diff): paste the old response on the left, the new one on the right, and toggle order-insensitive comparison (arrays of objects are then matched by `id`). It shows side-by-side or inline highlighting plus the path-level change list. Everything runs locally, so production payloads never leave your machine.

If you'd rather script it, the same idea works in a test: parse both sides, walk them by path, and treat arrays as multisets unless order matters for that field.

## FAQ

**Does key order matter?** No. Objects are compared by key, so `{"a":1,"b":2}` equals `{"b":2,"a":1}`.

**Can I export the result?** Yes. Copy or download the change list as a JSON report to attach to a PR or ticket.

---

Formaty is free and open source ([GitHub](https://github.com/kdkumawat/formaty)). [60-second demo](https://youtu.be/G1DtZnSSZEM).
