You've got two JSON payloads and you need to know if they're the same. Maybe one came from a staging API and one from production. Maybe it's a snapshot test that just started failing. Whatever the reason, comparing JSON is one of those tasks that looks like a five-minute job and quietly turns into an afternoon.

The trap is thinking string equality is enough. It almost never is.

What "comparing JSON" actually means

JSON is a data format, not a byte stream you should be diffing character by character. Two documents can be textually different but semantically identical:

{ "name": "Ada", "role": "dev" }
{ "role": "dev", "name": "Ada" }

These are the same object. Key order doesn't matter in JSON, so any comparison worth its salt has to treat them as equal. A naive text1 === text2 would flag a difference that doesn't exist.

So when we say comparing JSON, we usually mean a structural or semantic comparison: walk both trees, match keys regardless of order, and report only the values that genuinely differ.

When you actually need this

  • Debugging API responses. Your integration works locally but not against the vendor's sandbox. Diff the two responses and the culprit usually jumps out — an extra field, a null where you expected a string, a renamed key.
  • Snapshot and contract testing. When a test fails with "expected X received Y" dumped as two giant blobs, a real diff tells you which of 200 fields changed.
  • Config drift. Comparing environment configs across staging and prod is a classic. One missing feature flag can explain hours of confusion.
  • Migration validation. Moving data between systems? Diff a sample before and after to prove nothing was dropped or mangled.

The gotchas that bite everyone

Key ordering

Covered above, but worth repeating because it's the most common false positive. If your tool reports differences from reordered keys, throw it away.

Array order vs. set semantics

Arrays are the opposite problem. In JSON, arrays are ordered, so [1, 2, 3] and [3, 2, 1] are different. But often your data doesn't care about order — a list of tags, permissions, whatever. You have to decide per field whether order is meaningful. Most generic diff tools treat arrays positionally, which is the safe default, but it means a reshuffled list shows up as a wall of changes.

Number formatting

This one is sneaky. 1.0, 1, and 1e0 are the same number but different strings. Floating-point also drifts: a value serialized on one system as 0.1 might come back as 0.10000000000001 from another. Comparing numbers as parsed values rather than strings avoids the first problem; the second sometimes needs a tolerance.

Types that look alike

The string "42" is not the number 42. null is not the same as a missing key, and neither is the same as an empty string. These distinctions matter, and a good comparison surfaces them clearly instead of glossing over them.

Whitespace and encoding

Leading whitespace, trailing newlines, and BOM markers in a file can break a naive parse before you even get to the diff. Parse first, compare the parsed structures.

A short worked example

Say you're comparing a user object across two API versions:

// v1
{
  "id": 101,
  "name": "Ada Lovelace",
  "active": true,
  "roles": ["admin", "editor"]
}

// v2
{
  "name": "Ada Lovelace",
  "id": 101,
  "active": "true",
  "roles": ["editor", "admin"],
  "lastLogin": "2024-01-10"
}

A structural comparison should tell you three real things and ignore one non-issue:

  • id and name are reordered — ignore, semantically identical.
  • active changed from boolean true to string "true" — real bug, probably a serialization regression.
  • roles array order flipped — maybe a bug, depends on whether order matters here.
  • lastLogin was added in v2 — new field, likely expected.

Notice how the type change in active is the kind of thing that silently breaks a frontend expecting a boolean. A text diff buries it among cosmetic noise; a semantic diff puts it front and center.

If you'd rather not spin up a script for a one-off check, paste both objects into JSON Diff and it'll highlight added, removed, and changed keys while ignoring key order for you. Handy when you just need an answer before your coffee goes cold.

Doing it in code

For automated checks, most languages have a library. In JavaScript, a quick recursive equality check handles the common case:

function deepEqual(a, b) {
  if (a === b) return true;
  if (typeof a !== typeof b) return false;
  if (a === null || b === null) return false;
  if (typeof a !== 'object') return false;

  const keysA = Object.keys(a);
  const keysB = Object.keys(b);
  if (keysA.length !== keysB.length) return false;

  return keysA.every(k =>
    Object.hasOwn(b, k) && deepEqual(a[k], b[k])
  );
}

This gives you a yes/no answer with correct key-order handling. It does not tell you what differs, and it treats arrays positionally. For anything beyond a boolean, reach for a dedicated diff library that produces a change list — deep-diff in Node, DeepDiff in Python, or your test framework's built-in matcher.

Rules of thumb

  • Always parse before comparing. Never diff raw JSON strings.
  • Ignore key order; respect array order unless you have a reason not to.
  • Compare numbers as values, and add a tolerance if you're dealing with floats from different systems.
  • Watch for type mismatches — they're the bugs that hide in plain sight.
  • For quick manual checks use a visual tool; for CI, use a library that outputs a structured diff.

Get these right and JSON comparison stops being a time sink. The goal is simple: see the differences that matter, and none of the ones that don't.

Photo by Compagnons on Unsplash