TabOnly

Text & JSON Diff

Compare two files or pasted texts side by side — line diff, word-level highlights, and a structural view for JSON.

mode
view
a
b

A line diff and a JSON diff are different questions

Most diff tools ask one question: which lines changed? That is the right question for a log file or a source file, and this page answers it — a line-level diff, word highlights inside a changed line, a unified patch you can copy. But a config file, an API response or a database export is usually JSON, and JSON has a second, more useful question underneath the first one: did the data change, or did the formatting change? A file re-indented from two spaces to four, or re-saved with its keys in a different order, produces a line diff where every single line is marked changed — technically correct, and useless for the question you actually had. JSON mode answers it separately, and without touching what you pasted: it parses both sides and compares the parsed values, so key order and indentation cannot reach the result at all. If the two documents carry the same data, the page says so above the diff instead of leaving you to read a wall of changed lines and infer it. The two panes keep showing exactly what you gave them — canonicalising the text you are reading, to make the diff look tidier, would mean answering a question about a document you never wrote.

The structural view: what changed, and how

Alongside the text diff, JSON mode adds a second view, and it is the one that actually answers "did the data change": every difference between the two documents, compared as parsed values rather than as lines, listed by JSON Pointer path and tagged added, removed, changed or type-changed. This is the distinction a text diff structurally cannot make. A key that is missing and a key that is present but null read identically once serialised back to text — two very different bugs that look the same on a screen — and the structural view reports them as removed and changed respectively, so the difference is visible instead of inferred. The same goes for 1 versus "1": same characters once stringified with a two-character gap, entirely different types, reported as type-changed rather than a same-looking line.

Invisible differences, named

"These two lines are identical" is the most frustrating result a diff tool can give you, and it is almost always wrong in a specific, nameable way: a byte-order mark left over from a Windows editor, a non-breaking space (U+00A0) pasted from a web page where a regular space belongs, a zero-width space (U+200B) that copy-pasted in invisibly, or one file using CRLF line endings where the other uses LF. This page strips a leading BOM and normalises CRLF/CR to LF before diffing — both are reported, not silently applied — and for anything else that makes two visually identical lines compare unequal, it names the character and its Unicode code point rather than leaving you to guess.

The canonicalisation caveat

Canonicalising is not free, and the honest version of this page says where it costs you something. JSON.parse('1.0') produces the number 1 — verified this session, Object.is(JSON.parse('1.0'), 1) is true — because JSON numbers have no separate integer/float type for a parser to preserve. Once a value has gone through JSON.parse, there is no way to recover whether the source said 1 or 1.0; both canonicalise to the same string. If that distinction is load-bearing for you — a schema where an integer and a float field mean different things — read the text diff rather than the two parsed answers. It never parses your input, so a literal 1.0 versus 1 shows up as exactly the character difference it is; it is the sameness check and the structural view, both of which work on parsed values, that cannot see the distinction.

Why there is no "copy shareable link" here

Every other editor-shaped tool on this site offers a link that carries your input in the URL fragment. This page does not, and the reason is size rather than privacy: a shareable link for a diff has to carry two whole documents, and a URL that silently truncates when pasted into a chat client or an issue tracker is worse than no link at all. Copy the unified diff instead — it is one string, it is exactly what diff -u would show you, and nothing about pasting it anywhere truncates silently.

Do it without this tool

Every technique this page uses has a command-line equivalent, and knowing them is worth more than the page. All four were run on this machine before being printed here.

unified diff · the format this page's output matches
diff -u a.txt b.txt
# --- a.txt
# +++ b.txt
# @@ -1,3 +1,3 @@
#  one
# -two
# +TWO
#  three
git diff · two files that are not in a repository
git diff --no-index a.txt b.txt
# works even outside a git repo — --no-index is the whole trick
jq · canonicalise JSON, then diff normally
jq -S . a.json > a.norm.json
jq -S . b.json > b.norm.json
diff -u a.norm.json b.norm.json
# -S sorts object keys at every depth — the same rule this page's canonical mode uses
python · no jq installed
python3 -m json.tool --sort-keys a.json > a.norm.json
python3 -m json.tool --sort-keys b.json > b.norm.json
diff -u a.norm.json b.norm.json

The specs

This page's line-level engine is Myers 1986, An O(ND) Difference Algorithm and Its Variations — the shortest-edit-script algorithm nearly every diff tool descends from, including GNU and BSD diff and Git. The copyable output follows the unified diff format from POSIX/GNU diffutils: a ---/ +++ header naming both sides and one or more @@ -l,s +l,s @@ hunks, each giving the starting line and line count on both sides before the context/added/removed lines. The structural view's path column is RFC 6901, JSON Pointer — the same syntax RFC 6902, JSON Patch, uses to address a location for an add/remove/replace operation. This page does not emit JSON Patch, but the two specs share a vocabulary on purpose: a JSON Pointer path from this page's structural view is already in the shape a patch operation would need.

FAQ

Why does every line show as changed when the two files look the same?

Almost always an invisible character: a byte-order mark, a non-breaking space where a regular one belongs, a zero-width space, or one file using CRLF line endings while the other uses LF. A line diff compares bytes, not appearance, so two lines that look identical but carry a different byte are, correctly, different lines. This page strips a BOM and normalises CRLF to LF before comparing — and says so — but leaves anything else alone and names it specifically rather than silently editing your input to make the diff prettier.

Why do two JSON files with the same data show as completely different?

Because a text differ compares lines, and re-ordering an object's keys or re-indenting a file changes every line without changing what the document means. JSON mode canonicalises both sides first — keys sorted at every depth, consistent indentation — and diffs that instead. If the canonical forms match, this page says so directly rather than showing an empty-looking diff and leaving you to wonder whether it worked.

Does this page treat 1 and 1.0 as the same JSON number?

For canonicalisation, yes — and that is a real limitation, not an oversight. JSON.parse('1.0') produces the number 1 (verified this session: Object.is(JSON.parse('1.0'), 1) is true), because JSON numbers have no separate float type to preserve the .0 in. Once parsed, 1 and 1.0 are the same value; canonicalising and re-serialising both give "1". If that distinction matters — a schema where an integer and a float field mean different things — switch to raw text mode, which never parses the input and shows the literal byte difference.

What is the difference between a key that's missing and a key that's null?

One is the absence of a fact, the other is a fact whose value is null — and a line diff cannot tell you which happened, because a missing line and a line that changed to null look completely different on the page for reasons that have nothing to do with which one occurred. The structural view answers this directly: a key present on one side and absent on the other is added or removed, at its own JSON Pointer path; a key present on both sides whose value changed to or from null is changed (or type-changed, if the other side wasn't already null-shaped). Different entries, different meanings — conflating them is exactly the mistake a raw text diff makes by construction.

Why does this page never treat a reordered array as unchanged?

Because array order is data, not formatting. Two config files listing regions as ["eu-west-1", "us-east-1"] and ["us-east-1", "eu-west-1"] may or may not behave identically depending on what reads that array — a failover list cares enormously about which entry is first. JSON.stringify has no concept of "this array is a set"; neither does this page. The structural view compares arrays index by index, so a reordered array shows as a change at every index whose value moved, rather than being silently treated as equivalent to the original.

related tools