StackPractices
beginner By Mathias Paulenko

Diff JSON Objects

How to compare two JSON objects and find differences in Python, Java, and JavaScript.

Topics: data

Overview

Comparing two JSON objects sounds trivial until you try it on nested data. {"a": {"b": 1}} and {"a": {"b": 2}} aren’t equal, but == only tells you that they differ — not where, or how. A real JSON diff walks both trees and reports which keys were added, removed, or changed, at any depth. That’s what you need for snapshot tests, config drift checks, API contract monitoring, and migration audits.

This recipe shows three flavors across Python, JavaScript, and Java: a categorized report (added / removed / updated), a reversible RFC 6902 patch, and a dependency-free recursive diff.

When to Use

Use this recipe when:

  • Validating that a REST API response matches an expected snapshot
  • Detecting configuration drift between environment files (staging vs production)
  • Auditing a migration by diffing before/after data exports
  • Building an applyPatch flow that replays changes between systems

If you only need to merge two objects, see Merge JSON Files . Diffing and merging are related but opposite jobs.

Solution

Python

deepdiff gives a categorized report out of the box:

# pip install deepdiff
from deepdiff import DeepDiff

old = {"user": {"name": "Alice", "age": 30}, "roles": ["admin"]}
new = {"user": {"name": "Alice", "age": 31}, "roles": ["admin", "editor"]}

diff = DeepDiff(old, new)
print(diff)
# {'values_changed': {"root['user']['age']": {'old_value': 30, 'new_value': 31}},
#  'iterable_item_added': {"root['roles'][1]": 'editor'}}

With no third-party dependencies, a short recursive walk does the same job and is easier to customize:

def json_diff(old, new, path=""):
    """Return {path: change} for two JSON-like structures."""
    changes = {}
    if old == new:
        return changes
    if isinstance(old, dict) and isinstance(new, dict):
        for key in old.keys() | new.keys():
            if key not in old:
                changes[f"{path}/{key}"] = {"added": new[key]}
            elif key not in new:
                changes[f"{path}/{key}"] = {"removed": old[key]}
            else:
                changes.update(json_diff(old[key], new[key], f"{path}/{key}"))
    elif isinstance(old, list) and isinstance(new, list):
        for i, (o, n) in enumerate(zip(old, new)):
            changes.update(json_diff(o, n, f"{path}/{i}"))
        for i in range(len(old), len(new)):
            changes[f"{path}/{i}"] = {"added": new[i]}
        for i in range(len(new), len(old)):
            changes[f"{path}/{i}"] = {"removed": old[i]}
    else:
        changes[path or "/"] = {"old": old, "new": new}
    return changes

JavaScript

fast-json-patch produces an RFC 6902 patch you can replay with applyPatch:

// npm install fast-json-patch
import * as jsonpatch from 'fast-json-patch';

const oldDoc = { user: { name: 'Alice', age: 30 }, roles: ['admin'] };
const newDoc = { user: { name: 'Alice', age: 31 }, roles: ['admin', 'editor'] };

const patch = jsonpatch.compare(oldDoc, newDoc);
console.log(patch);
// [
//   { op: 'replace', path: '/user/age', value: 31 },
//   { op: 'add',     path: '/roles/1',  value: 'editor' }
// ]

For a simple added/changed/deleted report, deep-object-diff is lighter:

// npm install deep-object-diff
import { detailedDiff } from 'deep-object-diff';

console.log(detailedDiff(oldDoc, newDoc));
// {
//   added:   { roles: { '1': 'editor' } },
//   deleted: {},
//   updated: { user: { age: 31 } }
// }

Java

zjsonpatch emits the same RFC 6902 format through Jackson:

// Maven: com.flipkart.zjsonpatch:zjsonpatch
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.flipkart.zjsonpatch.JsonDiff;

public class JsonDiffExample {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode oldNode = mapper.readTree("{\"user\":{\"name\":\"Alice\",\"age\":30}}");
        JsonNode newNode = mapper.readTree("{\"user\":{\"name\":\"Alice\",\"age\":31}}");
        JsonNode patch = JsonDiff.asJson(oldNode, newNode);
        System.out.println(patch.toPrettyString());
        // [{"op":"replace","path":"/user/age","value":31}]
    }
}

If you want a plain report without the dependency, recurse over the Jackson tree — a shallow equals on the root only tells you the trees differ:

public static Map<String, Object[]> diff(JsonNode a, JsonNode b,
                                         String path, Map<String, Object[]> out) {
    if (a.equals(b)) return out;

    if (a.isObject() && b.isObject()) {
        a.fields().forEachRemaining(e -> {
            JsonNode av = e.getValue();
            JsonNode bv = b.get(e.getKey());
            if (bv == null) out.put(path + "/" + e.getKey(), new Object[]{av, null});
            else diff(av, bv, path + "/" + e.getKey(), out);
        });
        b.fields().forEachRemaining(e -> {
            if (a.get(e.getKey()) == null)
                out.put(path + "/" + e.getKey(), new Object[]{null, e.getValue()});
        });
    } else if (a.isArray() && b.isArray()) {
        int common = Math.min(a.size(), b.size());
        for (int i = 0; i < common; i++) diff(a.get(i), b.get(i), path + "[" + i + "]", out);
        for (int i = common; i < a.size(); i++) out.put(path + "[" + i + "]", new Object[]{a.get(i), null});
        for (int i = common; i < b.size(); i++) out.put(path + "[" + i + "]", new Object[]{null, b.get(i)});
    } else {
        out.put(path.isEmpty() ? "/" : path, new Object[]{a, b});
    }
    return out;
}

Explanation

JSON diffing is tree traversal. Both trees are walked node by node: object keys are checked for presence on both sides, array elements are compared by index, and scalars are tested for equality. What changes between libraries is the output:

  • Categorized reports (DeepDiff, deep-object-diff, the recursive walkers above) answer “what changed?” . Ideal for tests and audits.
  • RFC 6902 patches (fast-json-patch, zjsonpatch) answer “how do I turn A into B?” : a compact, replayable operation list. Use them for sync APIs and undo features.

One caveat: arrays are compared by position. [1, 2] and [2, 1] diff as two changes, even if order doesn’t matter in your domain. Sort or normalize first when that’s the case.

flowchart diagram: What do you need?

Worked Example

Diffing a staging config against production:

// staging.json
{ "db": { "host": "staging.db.local", "pool": 5 }, "cache": true, "features": ["search"] }

// prod.json
{ "db": { "host": "prod.db.local", "pool": 5 }, "cache": true, "features": ["search", "beta-ui"], "logLevel": "warn" }

The recursive diff reports three changes, not one blob of “files differ”:

/db/host        → staging.db.local  →  prod.db.local
/features/1     → added: "beta-ui"
/logLevel       → added: "warn"

That’s actionable: one expected environment difference, one feature someone forgot to disable, and a log level that never got documented. A single == check wouldn’t have told you any of that.

Variants

TechnologyLibraryApproachBest for
PythondeepdiffDeepDiff(old, new)Categorized reports, ignore_order, exclusions
Pythonstdlib recursivejson_diff() aboveZero deps, full control of the output shape
JavaScriptfast-json-patchcompare(old, new)RFC 6902 patches, reversible, compact
JavaScriptdeep-object-diffdetailedDiff(old, new)Simple added/updated/deleted split
JavazjsonpatchJsonDiff.asJson(a, b)RFC 6902 via Jackson, battle-tested
JavaJackson recursiondiff() aboveNo dependency, custom reporting

What Works

  • Normalize before diffing: sort keys and arrays when order is irrelevant; DeepDiff accepts ignore_order=True
  • Exclude volatile fields: timestamps, request IDs, and counters always differ — exclude_paths in deepdiff, or prune the keys before comparing
  • Pick the output for the job: categorized report for audits, RFC 6902 when you need to replay the change
  • Diff subtrees on huge documents: a deep diff on a 10MB payload is slow; compare the sections you care about or hash them first
  • Commit golden snapshots: snapshot tests need baseline files in version control, reviewed like code

Common Mistakes

  • Comparing floats directly: 1.0 and 1.00 serialize identically, but floating-point noise (0.1 + 0.2) creates phantom diffs . Round or use a tolerance
  • Ignoring array order on purpose: [1, 2] and [2, 1] are different JSONs; decide upfront whether order matters and normalize if it doesn’t
  • Diffing stringified JSON: JSON.stringify preserves insertion order, so {"a":1,"b":2} and {"b":2,"a":1} compare as different strings . Parse first, then diff
  • Treating null and missing as equal: {"a": null} and {} are semantically different; make sure your diff reports the key as removed, not changed
  • Logging raw diffs: a full structural diff of a large config is unreadable and can leak secrets . Summarize paths and values, and mask sensitive fields first

When NOT to Diff This Way

  • You only need equality: == (or assertEquals on parsed nodes) answers “same or not” without the report overhead
  • The payload is huge: deep diffs on multi-MB documents burn CPU; compare hashes or checksums and diff only when they disagree
  • Changes happen in real time: for streaming data, diffing every payload is too expensive . Validate incrementally or sample
  • You need the merged result: diffing tells you what changed; it doesn’t combine the objects. Use a merge recipe for that

See Also

Frequently Asked Questions

How do I ignore specific fields when diffing JSON?

Use exclusion rules. DeepDiff supports exclude_paths and exclude_regex_paths. fast-json-patch doesn't filter natively . Preprocess both objects by deleting ignored keys before comparison. For the recursive walkers, skip the key inside the loop.

Can I diff JSON while ignoring array order?

Yes. DeepDiff has ignore_order=True. In JavaScript, sort the arrays or convert them to sets before diffing. For the custom walkers, sort each array copy by a stable key before the index comparison.

What's the difference between a diff report and a JSON Patch?

A report (added/removed/updated) is human-readable and meant for inspection. A JSON Patch (RFC 6902) is a machine-readable operation list : add, remove, replace, move, copy, test. Designed to be replayed with applyPatch. Pick the report for tests and audits; pick the patch for sync and undo.

How do I generate a human-readable diff report?

DeepDiff's pretty() method produces readable output. For RFC 6902 patches, map op codes to verbs : replace → "changed", add → "added", remove → "removed". Print each with its path. The recursive walkers let you format each entry however you want.

Does diffing work on nested arrays inside objects?

Yes, as long as the algorithm recurses through both containers. The walkers above handle object-in-array and array-in-object nesting at any depth; what they can't do is match reordered array elements by identity — that's a harder problem most libraries solve only approximately.

How do I handle null versus a missing key?

They mean different things: {"a": null} explicitly sets a to null, while {} doesn't have the key at all. DeepDiff and the recursive walkers report the missing key as added/removed and the null as a value . That’s usually what you want. If your domain treats them as equal, normalize null fields out before diffing.