Skip to main content
Workflow · 7 min read

How to Compare Two JSON Files Properly

Why text diffs fail on JSON, how structural diffing works, and how to handle the genuinely hard part: matching up items inside arrays.

Comparing JSON with a line-based diff tool produces noise. Object key order is not meaningful in JSON, but it is extremely meaningful to diff. Reformat a file and every line changes. Reorder two keys and both lines change. Insert one array item at the top and every subsequent line is reported as different.

A structural diff parses both documents and walks them together, comparing values by path. Only real differences survive.

The four kinds of change

  • Added — the path exists only in the new document.
  • Removed — the path exists only in the old document.
  • Changed — the same path holds a different value of the same type.
  • Type changed — the same path holds a different JSON type: the number 10 became the string "10", or an object became null.

Arrays are the hard part

Objects are easy: match by key. Arrays have no keys, so the diff has to decide which item in the old array corresponds to which item in the new one. Get that wrong and inserting a single element at the front reports every element as changed.

There are three sensible strategies, and the JSON diff tool picks between them automatically:

  1. Match by identifier. If items are objects sharing a unique field — id, _id, uuid, key, slug, code, sku or name — match on it. This is by far the best result: an insertion reports one addition and nothing else.
  2. Longest common subsequence. For arrays without identifiers, up to a few hundred items, align them the way a text diff aligns lines. Genuine insertions and deletions are found; unchanged items in between are recognised as unchanged.
  3. Match by index. For very large arrays, compare position by position. Cheap and predictable, but an insertion at the front makes everything look different.

The practical takeaway: if you control the data, give array items a stable identifier. Every diffing tool, and every human reading the output, gets dramatically better results.

Comparing from the command line

# Normalise both files, then diff the text
diff <(jq -S . old.json) <(jq -S . new.json)

# Just ask whether they are equivalent
jq -S . old.json > /tmp/a; jq -S . new.json > /tmp/b; cmp -s /tmp/a /tmp/b && echo same

jq -S sorts object keys, which removes the key-order noise. It does not solve the array alignment problem — for that you want a real structural diff.

Where this comes up

  • API regression testing — capture a response before and after a deploy, diff them, and read the list of changes. Additions are usually fine; removals and type changes are the ones that break clients.
  • Configuration drift — compare the config a service is actually running against the config in the repository.
  • Migration verification — export the same record from the old system and the new one and compare field by field.
  • Debugging “it works on staging” — diff the two responses rather than reading both.

Reducing noise before you diff

Volatile fields — timestamps, request identifiers, signed URLs — differ on every request and drown the output. Strip them first:

jq 'del(.meta.generatedAt, .requestId, .. | .signedUrl?)' response.json

Then compare. What remains is the change you were actually looking for.