Just File Tools

JSON Diff

Structural diff of two JSON documents — added, removed, and changed paths

+2 added0 removed~2 changed
~version
before: "0.2.0"
after: "0.3.0"
~server.host
before: "localhost"
after: "0.0.0.0"
+server.tls
value: true
+features[2]
value: "text"

How to JSON Diff Online

Compare two JSON documents structurally — see exactly which paths were added, removed, or changed.

  1. Paste the original JSON into the left textarea and the new version into the right.
  2. If either side has a syntax error, the error appears under that textarea with the parser's exact complaint.
  3. When both sides parse, the diff renders below with a path for each change and a kind badge: + for added, − for removed, ~ for changed.
  4. Click into the change list to see before/after values for each modification.
  5. Identical documents show a green 'Identical' banner — useful as a sanity check after a refactor.
  6. Object key order does not matter. Array element order does.

About JSON Diff

JSON diff is one of those tools that sounds redundant — "just diff the text" — until you actually try it on real-world data and the text diff reports five hundred changes for a payload that has one meaningful difference. The reason is that JSON has a layer of representation freedom that text diffs do not understand: key order in objects, whitespace and indentation, escaping of strings, optional trailing commas (in JSON5), and choice of number representation (`1.0` vs `1`). Two JSON documents are equal if they parse to the same value, regardless of how those characters were written. A structural diff sees through the representation and reports only the genuine semantic differences.

The implementation here is a small recursive walker. It parses both sides with the native `JSON.parse`, walks the two trees in parallel, and for each pair of values checks: are they identical (recurse done), different scalars (report 'changed' with before and after), different types at the same path (report a single 'changed' with the whole subtree), or different shapes at the object/array level (recurse into matching children, report adds for keys only on the right, removes for keys only on the left). The output is a flat list of `{ path, kind, before, after }` records — easy to scan, easy to filter, easy to feed into a code-review workflow.

Three design choices worth knowing:

- **Object key order is ignored.** `{a:1,b:2}` and `{b:2,a:1}` produce zero diff entries. This is the right behavior — RFC 8259 says JSON object key order is unspecified, every parser is allowed to return keys in any order, and every consumer that depends on order is already broken. - **Array element order is significant.** `[1,2,3]` and `[1,3,2]` produce a diff. This matches RFC 7396 (JSON Merge Patch), JSON Schema, and every consumer in the wild. Multiset comparison ("are these arrays equal up to reordering?") is a different operation that needs domain knowledge to do correctly; this tool deliberately does not guess. - **null is not missing.** `{a: null}` and `{}` are different documents. The diff reports a removed key from the first to the second, which is what consumer code will see when it asks `obj.a` (`null` vs `undefined`).

The **path syntax** is plain JavaScript dot-and-bracket notation. `server.host` is `obj.server.host`. `users[0].email` is the email of the first user. Keys that contain dashes, spaces, or other non-identifier characters get quoted: `["user-id"]`. This means you can copy a path straight from the diff output and paste it into your debugger, your code, or a `jq` expression with a quick rewrite (`.server.host` for jq). Paths are JSON Pointer-adjacent but more human-readable.

Two common use cases. **API contract diffing**: you upgrade an API client and want to see how the response shape changed between versions. Paste both responses, get a clean list of added fields (forward-compatible), removed fields (breaking), and changed types (also breaking). **Config drift detection**: you have a checked-in config and a production-running config that have drifted apart. Paste both, get the exact list of paths where they differ — far more useful than a 200-line text diff with formatting noise.

Everything runs in your browser. The walker is around 80 lines of pure TypeScript and the test suite covers the tricky cases (key order independence, nested mutations, type changes at root, identifier vs. non-identifier path quoting). The page makes zero network requests during diffing. Internal API responses with sensitive payloads, environment-specific configs with hostnames and credentials — all stay in your tab.

Frequently Asked Questions

How is this different from a plain text diff?

A text diff treats both inputs as character sequences and reports differences in formatting, indentation, and key order alongside genuine value differences. A JSON diff parses both sides into structured values, then walks the trees and reports only changes that matter — `{a:1,b:2}` and `{b:2,a:1}` are the same JSON document, so the JSON diff reports zero changes. Use a JSON diff when you care about *meaning* (did this API response change?); use a text diff when you care about *bytes* (did the file on disk change?).

Why are arrays compared by index?

Because arrays in JSON are ordered sequences. `[1, 2, 3]` and `[1, 3, 2]` are different documents under the JSON spec (and under every JSON consumer in practice). Comparing arrays by index is what RFC 7396 (JSON Merge Patch) does, what most React component-equality checks do, and what JSON Schema validators do. Some specialized libraries support 'array as multiset' comparison; that is an opinion, not a standard, so this tool sticks with positional comparison.

What does the path syntax mean?

Paths use JavaScript dot-and-bracket notation, the same syntax you would type in code to reach a value. `server.host` means `obj.server.host`. `users[0].email` means the email of the first user. Keys that are not simple identifiers (contain dashes, spaces, etc.) are quoted: `["user-id"]`. The root of the document is shown as `(root)` when the entire value changed type (object replaced by array, etc.).

Is `null` the same as missing?

No. `{a: null}` and `{}` are different documents. The first explicitly states that `a` is null; the second says nothing about `a`. Most consumer code treats them differently — `obj.a` is `null` in the first case, `undefined` in the second. The diff reports `{a: null}` → `{}` as a removed key, and `{a: undefined}` is impossible in JSON because `undefined` is not a JSON type.

Why is type-mismatch reported as a single 'changed' instead of separate 'added' + 'removed'?

Because reporting `value at path X was an array, is now an object` is more useful than `array at X removed, object at X added`. Type changes at a single path are usually intentional and worth surfacing as a single semantic edit. The tool walks deeper into siblings only when both sides have compatible types at the current path.

What is the size limit?

The recursion is O(N) in document size — diffing a 10 MB JSON document is fast (under a second in the browser). The bottleneck is `JSON.parse`, which is itself extremely fast in modern engines. For documents with deeply nested cycles, JSON cannot represent them anyway, so the recursion always terminates.

Is my JSON sent to a server?

No. Parsing uses native `JSON.parse`, the diff algorithm is a small pure function in this page's bundle, and rendering is React. Zero network requests during the diff. Internal API responses, secrets-adjacent configs, anything sensitive — all stay in your tab.