Text & JSON Diff
Compare two files or pasted texts side by side — line diff, word-level highlights, and a structural view for JSON.
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.
diff -u a.txt b.txt # --- a.txt # +++ b.txt # @@ -1,3 +1,3 @@ # one # -two # +TWO # three
git diff --no-index a.txt b.txt # works even outside a git repo — --no-index is the whole trick
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
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.