Skip to content
Free Tool Arena

Developers & Technical · Guide · Developer Utilities

How to compare JSON

Why text diff fails on JSON, structural diffing, array-matching strategies, numeric precision, null vs missing, JSON Patch, contract testing.

Updated April 2026 · 6 min read

Diffing JSON sounds easy — just run a text diff. But JSON is structured, not lines, so a text diff flags formatting changes and key reorderings as real differences when they aren’t. A proper JSON diff understands objects (order-independent), arrays (order-dependent), and types. This guide covers how JSON diffing works, the gotchas around array matching, numeric precision, missing-vs-null, when text diff is actually the right tool, and how to use diffs in code review, API contract testing, and debugging.

Advertisement

Why text diff fails on JSON

Given these two JSON values:

{ "a": 1, "b": 2 }
{ "b": 2, "a": 1 }

A text diff says they’re different. A JSON diff says they’re identical. Object key order is not semantic in JSON.

Same for formatting — {"a":1} and { "a": 1 } are identical. And trailing newlines, indentation, whitespace inside strings (actually that’s semantic — careful).

Structural diff basics

A JSON diff walks the tree recursively and reports changes in semantic terms:

Added: key present in new, not in old.

Removed: key present in old, not in new.

Changed: key in both, values different.

Unchanged: key in both, values identical.

For nested structures, drill into the object/array recursively and report paths like user.address.city instead of line numbers.

Array diffing is hard

Object keys are unique; array positions aren’t. If you’ve got:

old: [{ "id": 1 }, { "id": 2 }, { "id": 3 }]
new: [{ "id": 2 }, { "id": 3 }]

Did position 0 change from {id:1} to {id:2}, and position 2 get removed? Or did position 0 get removed entirely?

Positional matching: compare by index. Simple, but sensitive to insertions.

Keyed matching: tell the differ to match by a specific field (e.g., id). Matches {id:2} to the same {id:2} regardless of position.

LCS (longest common subsequence): treat array like text; find the best edit sequence. Good for ordered lists.

Picking the right strategy depends on what the array represents. Tell the tool which field is the identity, and it does the right thing.

Numeric precision gotchas

JSON numbers don’t carry type info. Is 1 the same as 1.0? Most tools say yes. Is 0.1 + 0.2 equal to 0.3? Your language says 0.30000000000000004, so maybe no.

When comparing numbers, use epsilon tolerance for floats generated by computation. Exact match only for integers and hand-typed decimals.

Beware JSON number overflow: JS parses 9007199254740993 as 9007199254740992 (loses a bit). If your data has IDs above 2^53, use strings for them — and configure your differ to treat "12345" and 12345 as different.

Missing vs null vs undefined

JSON has null but no undefined. A key being absent is different from a key being present with value null:

{ "a": null }   vs   {}

Some APIs treat these identically; some don’t. Your diff tool needs a consistent stance:

Strict: missing and null are different. Safer for contract testing.

Loose: missing and null are the same. Useful for comparing API responses where optional fields drop out.

Viewing the diff

Side-by-side: old on left, new on right, with changes highlighted. Best for small objects.

Unified (path-based): list of changes by JSON path: ~user.email: "old@x" -> "new@x". Compact for large objects with few changes.

JSON Patch (RFC 6902): structured output of operations: [{"op": "replace", "path": "/user/email", "value": "new@x"}]. Machine-readable; can be applied to the old object to produce the new.

JSON Merge Patch (RFC 7396): simpler format where the patch is just a partial JSON object. Can’t represent array operations well; good for simple object updates.

Common use cases

API contract testing: hit a known endpoint, compare response against a snapshot. Alerts when schema drifts.

Config file review: compare two config versions to see what changed. Much cleaner than git diff on formatted YAML/JSON.

Sync debugging: compare source and target of a sync to find what’s missing or malformed.

State snapshots in tests: Jest’s toMatchSnapshot does a text diff; a JSON diff is cleaner for structured state.

When to use text diff instead

Formatting matters: checking whether a JSON file’s indentation changed — text diff.

Comments in JSON5/JSONC: JSON diff tools strip comments; text diff preserves them.

Version control: git diff on JSON files. Ugly, but already integrated. For PR review, a structural diff add-on (like Gitiles’ json-diff) is nicer.

Common mistakes

Diffing JSON as text. Formatting noise buries real changes. Use structural diff.

Ignoring array-order semantics. If order doesn’t matter (e.g., tags), tell the diff. If it does matter (e.g., steps in a workflow), keep positional diff.

Treating null and missing as same. APIs distinguish them. Your contract test should too.

Comparing pretty-printed JSON. If one side is minified and the other is pretty-printed, text diff shows every line as changed. Normalize format first.

Exact-match floats. Computed floats rarely match byte-exactly. Use tolerance.

Run the numbers

Compare JSON structures instantly with the JSON diff checker. Pair with the JSON formatter to normalize before diffing, and the text diff checker when you need a plain line diff.

Advertisement

Found this useful?Email