Comparing two JSON objects sounds like a solved problem. You've got a string, you've got another string, you diff them. Except that's almost never what you actually want, and the naive approach breaks in ways that waste an afternoon.

I've hit this in API testing, config validation, and debugging "why did production return something different from staging?" more times than I can count. Here's what actually matters.

What "comparing JSON" really means

There are two fundamentally different comparisons, and mixing them up is the root of most confusion.

  • Textual comparison treats JSON as a string. Whitespace, key order, and trailing commas all count. Useful for checking that two files are byte-identical, useless for checking that two objects represent the same data.
  • Structural (semantic) comparison parses both sides and compares the resulting data structures. Key order doesn't matter, formatting doesn't matter — only the actual values and structure do.

Ninety percent of the time you want structural comparison. The classic mistake is running a plain string diff on two API responses, seeing a wall of red, and panicking — when in reality the server just serialized the keys in a different order.

When you'd reach for it

A few situations where JSON comparison earns its keep:

  • API contract testing. You captured a known-good response and want to assert the current one matches, ignoring cosmetic differences.
  • Config drift. Comparing a deployed config against the version in your repo to spot what someone changed by hand.
  • Debugging serialization bugs. Your object goes in, comes out slightly different, and you need to see exactly which field mutated.
  • Reviewing generated output. When a tool spits out JSON — OpenAPI specs, Terraform state, snapshot tests — a semantic diff tells you what genuinely changed between runs.

The gotchas that get everyone

Key order

JSON objects are unordered by definition. {"a":1,"b":2} and {"b":2,"a":1} are equal. Any comparison that flags these as different is doing textual comparison, and it's lying to you.

Number representation

This one is nasty. 1.0, 1, and 1e0 may or may not be considered equal depending on the parser. And floating point makes it worse — 0.1 + 0.2 famously doesn't equal 0.3. If your JSON went through a language that reads all numbers as floats, you can get spurious diffs on values that are logically identical.

Array order

Arrays are ordered, so [1,2,3] is not equal to [3,2,1]. But often you have an array that's semantically a set — a list of tags, permissions, IDs — where order shouldn't matter. No generic diff knows that; you have to normalize (sort) those arrays yourself before comparing.

Null vs missing

{"name": null} and {} are different. Whether they should be treated as different depends entirely on your domain. Decide deliberately.

Type coercion

The string "5" is not the number 5. Obvious when written out, easy to miss when it sneaks in from a form field or a loosely-typed API.

A worked example

Say you're testing an endpoint that returns a user. Here's the expected payload and what the server actually returned:

// expected
{
  "id": 42,
  "name": "Ada Lovelace",
  "roles": ["admin", "editor"],
  "active": true,
  "metadata": null
}

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

A string diff screams that everything's wrong. A structural diff shows the truth: the only real differences are the roles array order and the missing metadata key. Whether those are bugs is your call — but now you know exactly what to look at.

If you're doing this by hand or in a quick debugging session, paste both blobs into a visual comparison tool like Devsly's JSON Diff, which parses both sides and highlights the meaningful changes instead of drowning you in formatting noise.

In code, a normalized comparison in JavaScript might look like this:

function normalize(value) {
  if (Array.isArray(value)) {
    // sort only if order is not significant for this field
    return value.map(normalize);
  }
  if (value && typeof value === 'object') {
    return Object.keys(value)
      .sort()
      .reduce((acc, key) => {
        acc[key] = normalize(value[key]);
        return acc;
      }, {});
  }
  return value;
}

function jsonEqual(a, b) {
  return JSON.stringify(normalize(a)) === JSON.stringify(normalize(b));
}

That handles key order by sorting object keys before stringifying. It does not reorder array elements, because in the general case that would be wrong. If a specific field is a set, sort it explicitly before comparing:

expected.roles = [...expected.roles].sort();
actual.roles = [...actual.roles].sort();

Notice we clone with the spread before sorting — mutating the original object during a comparison is a great way to create bugs that only appear on the second run.

Choosing the right tool for the job

For quick, one-off inspections, a visual diff beats writing code every time. You see the two documents side by side, changes highlighted, and you're done in seconds.

For automated tests, use a proper deep-equality assertion from your framework — expect().toEqual() in Jest, assertEqual on parsed objects in Python, and so on. These compare parsed structures, so they already ignore key order and whitespace. Never assert on stringified JSON unless byte-exactness is genuinely what you're testing.

For serious contract testing where you want to allow certain fields to vary (timestamps, generated IDs), look at schema-based matching or partial-match assertions rather than trying to hand-roll every exception.

A quick checklist

  • Parse before you compare — never diff raw strings unless you mean to.
  • Decide up front how you treat null vs missing keys.
  • Normalize arrays that are logically sets.
  • Watch numeric types, especially after a round-trip through another language.
  • Clone before sorting; don't mutate the inputs.

Get those five things right and JSON comparison stops being a source of mysterious red text and becomes what it should be: a fast, reliable way to see exactly what changed.

Photo by Mohammad Rahmani on Unsplash