JSON diff

Compare two JSON documents and see what was added, removed or changed. Key order, indentation and minifying are ignored, and nothing leaves your browser.

First JSON
Second JSON
Result

Paste JSON into both panes to compare them

Comparing the data, not the text

A line-by-line comparison answers a question about the writing: which lines of this file are not lines of that one. That is the right question for prose and for source code, and the wrong one for JSON, because a JSON document has no single correct way of being written. Reorder the members of an object, indent it differently, or minify one side, and every line changes while the data does not move at all. This tool compares what the two documents parse to, so what it reports is what actually changed. Below is one document written twice — minified on the first line, then pretty-printed with its members in another order.

{"name":"checkout","port":8080,"tags":["a","b"]}

{
  "port": 8080,
  "tags": ["a", "b"],
  "name": "checkout"
}

Those two share not one line, and this tool reports no difference between them at all: the same members, the same values, the same order inside the list. The practical consequence is that you never have to reformat a document to make a comparison work — which would mean changing the very thing you set out to compare.

What never counts as a difference

Four things about how a document is written are ignored, and they are ignored unconditionally. There is no control that turns any of them back on, because a tool that could would be two tools with two meanings to explain, and the reader would have to know which one produced the answer in front of them.

  • The order the members of an object are written in. {"a": 1, "b": 2} and {"b": 2, "a": 1} hold the same data, so a serialiser that emits its keys in a different order on every run never shows up here as a change.
  • Whitespace of every kind — the indentation, the line breaks, the space after a colon. A minified document and a pretty-printed one carrying the same data are the same document twice.
  • How a number is spelled. 1e3 is 1000 and 1.50 is 1.5, because a JSON number has a value and no spelling: two documents differing only there hold the same numbers, and nothing at all is reported about them.
  • How a character inside a string is escaped. The six characters \u0041 and the letter A are the same one-character string.

Two things that look like they belong on that list are deliberately not on it. The order of a list’s elements is data — [1, 2] and [2, 1] are different documents — and a member whose value is null is not the same as a member that is not there at all, so one against the other is reported rather than quietly passed over.

Added, removed, changed — and a change of type

Every row of the result is exactly one of three kinds, and between them they cover everything the comparison can find. Take these two documents:

{"host":"api.example.com","port":8080,"debug":true}

{"host":"api.example.com","port":"8080","retries":3}
  • Changed at $['port']: the member is in both documents and its value is not the same, so both values are shown. This one is also marked as a change of type, because 8080 is a number and "8080" is a string.
  • Removed at $['debug']: the member is in the first document and not in the second.
  • Added at $['retries']: the member is in the second document and not in the first.

The member named host is in both with the same value, so it is not reported at all. A change of type is a property of a change rather than a fourth kind of row: it is the drift a schema notices and a person reading two files usually does not, so it is called out where it happens instead of being left for you to spot.

Where a difference sits, written as a path

Every row names the location it sits at, and it names it as a path into the document rather than as a line number — a line number would be a fact about the writing, which is the thing this tool has just finished ignoring. The paths are RFC 9535 Normalized Paths, the one spelling this site uses wherever a JSON location has to be written down.

  • $ is the whole document, $['port'] is the member named port in it, and $['hosts'][1] is the element at index 1 of the list under hosts. A member is always written in brackets and quotes, so a name containing a dot, a space or a bracket of its own needs nothing special.
  • A path is a query you can run. Paste one into this site’s JSONPath tester together with the document it belongs to and it selects exactly that node, which is the quickest way to read a difference in its surroundings.
  • It is a query against the document the node is actually in: an added member’s path runs on the second document and a removed member’s on the first. Nothing here ever names a node that is not there.

A changed row can carry two paths, and that is the matching in the next section showing through: once a list has gained or lost an element, the same element sits at a different index on each side. Where both paths are the same — which is nearly always — only one of them is shown.

How two lists are matched up

Two objects are compared member by member, which is straightforward because a member has a name. Two lists have no names, only positions, and comparing them position by position is what makes most comparison output unreadable: insert one record at the front of a long list and nothing is where it was any more, so a comparison that reads position as identity reports the whole list as different — true in the narrow sense and useless in every other. So the elements are matched up first, by what they are rather than by a field you nominate, and only what the matching leaves over is reported.

{"hosts": ["alpha", "beta", "gamma"]}

{"hosts": ["alpha", "staging", "beta", "gamma"]}

That is one row: staging was added at $['hosts'][1]. The elements named beta and gamma moved along by one and are not mentioned, because moving is not changing. The same matching is what lets a single edited field inside one item of a list be reported as a change at that field, rather than as the whole item having been replaced by a different one.

Order still counts, and the matching does not pretend otherwise: [1, 2] against [2, 1] comes back as 1 removed from the front and 1 added after 2, which is what happened. Two lists can also be too different for matching them up to be worth its cost, and past that point the comparison falls back to going position by position and says so at the top of the result — so a noisy answer is explained rather than mysterious.

The report and the tree

The same comparison is offered in two renderings and the switch between them recomputes nothing: the tree is drawn from the very list of differences the report lists, so the two cannot disagree about what changed or about how much of it there is.

  • The report answers what changed: one row per difference, each carrying its path, its kind and its value on each side. It is the view to scan when you want the whole list in front of you.
  • The tree answers where in the document: the two documents merged into one outline, with only the parts holding a difference open. Every run of consecutive children holding nothing collapses to one line saying how many it stands for — the pair in the section above draws as five rows, the document, the list inside it, a folded line standing for one element, the added element, and a folded line standing for two.
  • The count beside the result’s heading is every difference there is. When the list on screen is cut short for length that count does not move, and a control at the foot of the list shows the rest.
~! $['port'] 8080 -> "8080"
- $['debug'] true
+ $['retries'] 3

That is what the copy control produces for the two documents in the third section: a marker for the kind, then the path, then the values with an arrow between them — a plus for an added member, a minus for a removed one, a tilde for a change, and a tilde with an exclamation mark where the type changed too. It is written in symbols rather than words on purpose, so a pasted result is the same text whichever of the site’s languages you were reading in, and it shortens nothing: a value the screen cut is whole in there.

What parsing costs, and what the tool says out loud

Comparing what two documents parse to means living with what parsing costs, and parsing JSON is not lossless. That matters more here than anywhere else on this site, because the answer a comparison tool may least afford to give is no difference between two documents that really do differ. So each pane’s text is read a second time as text, and what the parse dropped is reported beside the comparison rather than swallowed.

  • A member name written twice in the same object. Only the later copy survives the parse, so only the later copy was ever compared and the earlier one was seen by nothing.
  • A number too long to survive as a double. Two different literals can parse to the same value, and identifiers are exactly where that happens: 12345678901234567890 and 12345678901234567891 are different integers and one parsed number, so the comparison on its own would call two documents identical when their ids differ.
{"user": {"id": 1}, "user": {"id": 2}}

That document parses to {"user": {"id": 2}}, and the tool says so under the pane it was pasted into, with the line and column of the first offender and how many there are in all. Neither loss stops the comparison; both sit beside it. A document that will not parse at all is reported in the same place, with the position of the problem wherever there is one to give, and the two panes are read independently — so two broken documents produce two errors at once rather than one after the other. A document nested far deeper than a value can be drawn is refused outright, which is a refusal and not a hung tab.

Why nothing you paste leaves this tab

Both documents stay in your browser. There is no upload, no request and no server that could hold a copy: the comparison is arithmetic running inside the page you already loaded, which is also why it keeps working with the network switched off. That is what makes it safe to paste a production response, a customer record or a signed payload into it.

Nothing is stored either. Reload the page and both panes are empty; there is no history, no account and nothing to delete afterwards. The only thing that ever leaves is whatever you copy out yourself.

Frequently asked questions

Does this ignore key order?
Yes, always, and that is the reason the tool exists. Two objects holding the same members with the same values are identical however they were written, so a serialiser that emits its keys in a different order on every run never shows up as a change. Indentation, minifying, how a number is spelled and how a character is escaped are ignored for the same reason, and none of it is a setting you could have switched the other way.
Is a member set to null the same as a member that is not there?
No, and there is no option to treat them as the same. {"note": null} against {} is reported as $['note'] removed. A field that is present and empty is a different statement from a field nobody sent — every schema language and every statically typed language tells the two apart — so collapsing them here would throw away something the documents are actually saying.
Are [1, 2] and [2, 1] reported as the same?
No. A JSON array is ordered, so two lists holding the same elements in a different order are two different documents, and calling them equal would be this tool saying something false about the format. What you get is one removal and one addition: 1 was removed from the front and added again after 2. Objects are the other way round, because their members are named rather than positioned.
What does the path on each row mean?
It is where the difference sits inside the document, written as an RFC 9535 Normalized Path: $ is the document itself, $['port'] a member of it, $['hosts'][1] the element at index 1 of the list under hosts. It is also a query you can run — paste it into this site’s JSONPath tester along with the document it belongs to and it selects exactly that node, and nothing else.
Why did one row show two different paths?
Because matching two lists up can leave the same element at a different index on each side. Compare ["alpha", "beta"] with ["intro", "alpha", "beta!"] and you get two rows: intro was added at $[0], and beta changed to beta! — which is $[1] in the first document and $[2] in the second. Where the two paths are the same, and they usually are, only one of them is shown.
What does a change of type mean on a row?
That the value is not merely different but a different kind of thing: a number that became a string, a value that became null, an object that became a list. A port written 8080 on one side and "8080" on the other is the commonest case, and it usually means a serialiser or a client changed rather than the data. It is marked on the row because an edited value and a drifting contract want different responses from you.
My two documents look identical but one writes the same key twice. What happens?
The parse keeps the later copy and drops the earlier one, so the comparison sees a single member and may well report no difference. The tool reads the raw text separately for exactly this reason and says, under that pane, that a name is written twice, how many times in all, and where the first one is. The same second reading catches an integer too long to survive as a double, which is the other way two documents that differ can be parsed into two that do not.
Can I get a patch I can apply?
No, and that is a decision rather than something missing. A JSON Patch is applied one operation after another, so the indices inside it shift as it runs; emitting one is a promise about a program you would run against your own data, which is a much larger claim than describing what differs. What you can take away instead is the report as text, from the copy control — the same in every site language, and shortened nowhere.
Is anything I paste sent to a server?
No. Both documents are compared in your browser and neither leaves it: no upload, no request, and nothing kept between visits. That is what makes it safe to paste a production response in, and it is why the tool still works with the network switched off. Reload the page and both panes are empty again.

Related tools

  • Text diff

    When the change you want to see is in the writing rather than in the data — an indentation pass, a key that moved, or two files that are not JSON at all — line by line is the comparison that shows it. That page does that one, with options for whitespace and case and a patch you can copy.

  • JSON formatter

    Validate and beautify JSON, with clear error locations.

  • SQL formatter

    Format and beautify SQL — multiple dialects.

  • XML formatter

    Format XML and check it is well-formed — beautify or minify.