To compare two JSON files usefully, compare their structure, not their text: parse both documents, walk them side by side, and report three kinds of result — paths that were added, paths that were removed, and paths whose value changed. A plain text diff gets JSON wrong in both directions: it flags documents as different when only the key order or indentation changed, and it buries a one-character type change inside a reflowed line. A structural compare ignores key order inside objects because RFC 8259 defines an object as an unordered collection, treats array order as significant because arrays are ordered, and reports a value that changed from the number 12.99 to the string "12.99" as a change — which it is, even though the two look identical on screen. Our JSON Studio includes a structural compare that works exactly this way, in your browser.

Text diff versus structural diff

A text diff compares lines of characters. That is the right model for prose and source code, and the wrong model for a data format whose layout is arbitrary. Run a text diff on two copies of the same document — one formatted with two-space indents, one minified — and every line lights up, although nothing changed. Reorder the keys of an object, which any JSON generator is free to do, and the text diff reports edits on lines that carry identical information.

A structural diff compares values at paths. It parses both texts first — so both must be valid JSON; if either fails, fix that first with the workflow in How to Format and Validate JSON. Then, starting at the root path $, it asks at each point: are these the same type? If they are objects, which keys exist on each side, and do the shared keys hold equal values? If they are arrays, what happens element by element? The output is a list of paths such as $.user.roles[1], each labelled added, removed or changed, with the old and new values attached. Paths in the examples below use the same convention as JSONPath in RFC 9535: $ is the root, .name steps into an object, and [n] steps into an array.

Worked example 1: key order is not a change

We ran these two documents through the studio's structural differ, in Node, using the same code the compare tab runs:

// A
{"name": "Ada", "price": 12.99, "tags": ["a", "b"]}
// B
{"tags": ["a", "b"], "price": 12.99, "name": "Ada"}

(The // A labels are annotations for this article, not part of the files — comments are not valid JSON.) The result was:

added:   []
removed: []
changed: []
identical: true

A text diff would have reported changes on almost every line. The structural result is the truthful one: an object is a set of name/value pairs, and the set is the same. This is also why you should not panic when an API starts returning keys in a new order after a deploy — and why a snapshot test that compares raw JSON text will fail on that deploy even though the data did not change.

Worked example 2: an API response that really changed

Now a pair where something did happen:

// A
{"id": 7, "name": "Ada", "price": 12.99, "stock": 5, "legacy": true}
// B
{"id": 7, "name": "Ada", "price": "12.99", "stock": 4, "currency": "GBP"}

The differ reported four results:

Kind Path Values
Added in B $.currency "GBP"
Removed from A $.legacy true
Changed $.price 12.99 → "12.99"
Changed $.stock 5 → 4

Read them in order of danger, not in the order listed. The $.stock change is a normal data change. The added currency and removed legacy are schema changes: a consumer reading legacy will now get nothing, and a consumer that ignores unknown keys will never notice currency arrived. The $.price change is the serious one. The value looks the same in a log file, but its type flipped from number to string. Code that adds prices will now join text instead of adding numbers, and a strict comparison against 12.99 will fail. A text diff shows this as one edited line among many; the structural report isolates it as a change at a named path with both values and both types visible.

Arrays: order is information, until it is not

Arrays are ordered, so by default the differ compares them position by position. We tested a tag list that only changed order:

// A
{"tags": ["fiction", "classic"]}
// B
{"tags": ["classic", "fiction"]}

With the default setting, the result was two changes: $.tags[0] changed from "fiction" to "classic", and $.tags[1] changed from "classic" to "fiction". Nothing was added or removed, but the report is noisy — it describes a reshuffle as two edits. With the compare's "ignore array order" option switched on, the same pair returned identical: true, with empty added, removed and changed lists.

Which setting is right depends on what the array means:

  • Order is meaning — steps in a process, ranked results, a route, a playlist you curated. Compare strictly, and read a reorder as a real change.
  • Order is accident — tags, permission sets, a list of category IDs assembled from a database query with no sort. Ignore order, or you will chase phantom changes after every deploy.

A nested case shows how paths keep array results readable. Comparing {"user": {"roles": ["reader", "editor"]}} with the same document where the second role is "admin", the differ reported exactly one change: $.user.roles[1], "editor" → "admin". The path tells you where to look; you do not have to scan the document for the difference yourself.

How to read a compare report

  1. Count first. Three results and three thousand results call for different responses. A huge report usually means a formatting or ordering shift somewhere upstream, not three thousand real edits — re-run with array order ignored and see what survives.
  2. Separate schema from data. Added and removed paths are schema events. Changed paths with the same type on both sides are data events. Changed paths where the type differs — number to string, object to null, anything to a different kind — are the ones most likely to break a consumer, so review them individually.
  3. null is a value, absence is not. A key present with value null and a key missing entirely are different states, and a structural differ reports them differently: present-to-absent is a removal, null-to-value is a change. Systems that treat them as the same are making an assumption you should make deliberately.
  4. Save the inputs with the report. A list of paths is only checkable if you keep the exact two documents that produced it. JSON files are small; keep both.

When the two things you are comparing are not JSON at all — a contract draft, a policy page, a set of release notes — use a text comparison instead; How to Compare Two Documents and Find Changes covers that workflow, and structured data with JSON-LD shows a case where comparing the JSON embedded in two versions of a page catches changes the visible text never shows.

Frequently asked questions

Why does my text diff say everything changed when the data looks the same?

Because text diff compares lines, and reformatting, minifying or reordering keys rewrites lines without changing values. Parse both files and compare them structurally. If the structural report says the documents are identical, they are — the text difference is layout, not content.

Should object key order ever matter?

Not in JSON itself: an object is an unordered collection of name/value pairs. It can matter in practice if a consumer is careless — for example, code that reads the "first" key, or a signing scheme that hashes the raw text. Those are properties of the consumer, not the format, and a structural compare is still the right way to find out whether the data changed.

What does it mean when a value changed from 12.99 to "12.99"?

The type changed from number to string. Arithmetic, sorting and strict equality all behave differently for the two forms, so treat it as a breaking change until you have checked every consumer. It often appears when a value starts coming from a form field, a CSV column or a database column defined as text.

Can I compare JSON files larger than a few megabytes?

Parse-and-walk comparison needs both documents in memory, and browser tools set explicit limits to stay responsive — JSON Studio caps interactive compare at 1 MB and directs larger documents to its format and validate functions. For very large files, compare on the command line in your own environment, or split the document by its top-level keys and compare section by section.

Is a missing key the same as a key set to null?

No. {"a": null} says the key exists and its value is null; {} says nothing about a at all. A structural differ reports the move between them as a removal or an addition, not a change, which is the correct reading — many APIs use absence and null to mean different things.

References