ForgeHub

JSON

Comparing JSON arrays: by position or by identity?

There are four common ways to diff JSON arrays: by index (position 0 against position 0), by an identity key (match objects by a field such as id), by sequence alignment (the longest-common-subsequence approach line diffs use), and as an unordered set. Comparing by index is the simplest and most predictable, but one item inserted at the front makes every later position look changed. If order does not matter in your data, sort both arrays by a stable key before comparing, or turn them into objects keyed by id.

Why arrays are the hard part of a JSON diff

Objects are easy to diff because members have names. $.settings.theme in one document clearly corresponds to $.settings.theme in the other, and key order is irrelevant (see why key order and whitespace shouldn't show up in a JSON diff).

Array elements have no names, only positions. RFC 8259 defines an array as an ordered sequence of values, so order is part of the data. But many real arrays, such as a list of users, tags or permissions, are really collections where order is incidental. A diff tool has to decide which element in the original corresponds to which element in the updated document, and that decision is the whole difference between the strategies below.

The example: one item inserted at the front

Every strategy below uses the same pair of documents. The only real change is that a new user was added at the start of the array.

// before.json
{"users": [
  {"id": 1, "name": "Ada"},
  {"id": 2, "name": "Grace"},
  {"id": 3, "name": "Linus"}
]}

// after.json
{"users": [
  {"id": 4, "name": "Margaret"},
  {"id": 1, "name": "Ada"},
  {"id": 2, "name": "Grace"},
  {"id": 3, "name": "Linus"}
]}

A human reads this as "one user added". Whether a tool agrees depends on how it matches elements.

Comparing JSON arrays by index (position)

How it works: element [0] is compared with element [0], [1] with [1], and so on. If the updated array is longer, the extra positions are reported as added; if it is shorter, the missing positions are reported as removed. Objects inside the array are compared field by field.

This is the strategy JSON Mirror uses. For the example above it reports seven entries:

changed  $.users[0].id     1 -> 4
changed  $.users[0].name   "Ada" -> "Margaret"
changed  $.users[1].id     2 -> 1
changed  $.users[1].name   "Grace" -> "Ada"
changed  $.users[2].id     3 -> 2
changed  $.users[2].name   "Linus" -> "Grace"
added    $.users[3]        {"id": 3, "name": "Linus"}

Every later position reports a change, and the last original element reappears as an "added" entry at the end. Nothing here is wrong: position 0 really did hold Ada and now holds Margaret. It is just not the summary a person wants.

Why it is still a reasonable default: it is predictable. Every path in the output points at a real location in both documents, so you can look up $.users[2] in either input and see exactly the values shown (more on that in reading JSON paths in a diff). It needs no configuration, no schema knowledge and no heuristics, the same input always produces the same output, and it runs in linear time. Appends and in-place edits, which are very common, produce clean results.

Limitations: inserts and deletes near the start cause a cascade of "changed" noise, and it does not detect moves. Swapping two items reports changes at both positions.

Diffing arrays of objects by id (identity key)

How it works: you name a field that uniquely identifies each element, such as id, sku or email. The tool matches elements with the same key value regardless of position, then compares matched pairs field by field. Keys present only in the updated array are added; keys present only in the original are removed.

For the example, the result is a single entry, shown here in illustrative notation:

added    users[id=4]   {"id": 4, "name": "Margaret"}

Pros: the output matches human intent, moves are ignored, and an edited user is reported as a change to that user rather than to a position. With a hash map it is roughly linear in cost.

Cons: it needs schema-specific knowledge, so a generic tool has to be told which key to use, and different arrays in one document may need different keys. Keys must be present and unique; duplicate or missing ids silently merge or drop elements. It does not apply to arrays of plain values like ["a", "b"], and it hides reordering, which matters if order is meaningful (a priority list, for instance).

Sequence alignment: how line diffs handle arrays

How it works: the tool finds the longest common subsequence (LCS) of elements that are exactly equal in both arrays, then reports everything outside it as inserted or deleted. Myers' algorithm, used by git diff and GNU diff, is an efficient way to compute this kind of minimal edit script.

You can see it on the example by printing one array element per line and running an ordinary line diff:

diff <(jq -cS '.users[]' before.json) <(jq -cS '.users[]' after.json)

0a1
> {"id":4,"name":"Margaret"}

One insertion before the first line: the right answer, with no key required.

Pros: it handles inserts and deletes anywhere without a schema, and preserves order information.

Cons: matching uses exact equality, so an element that was both moved and edited appears as one deletion plus one insertion. Reordering also shows up as delete/insert pairs rather than moves. It costs more: classic LCS is O(n × m), and Myers is O((n + m) × D), where D is the size of the difference, which is fast for small changes but slows down for very different arrays. When several alignments are equally short, which one is chosen depends on the implementation.

Comparing JSON arrays as a set (ignore order)

How it works: each array is treated as an unordered collection (strictly, a multiset if duplicates count). Elements that appear in both are ignored; the rest are reported as added or removed. The example again produces one "added" entry for Margaret.

Pros: no key needed, and order differences disappear entirely. It is a good fit for tags, roles and permission lists.

Cons: an edited element is reported as one removal plus one addition, with no field-level detail. Real order changes become invisible, and you must decide whether duplicates matter.

JSON array diff strategies compared

StrategyMatches elements byFront insertion in the exampleNeeds a key?Typical costMain drawback
By indexPosition6 changed + 1 addedNoLinearCascading "changed" noise; no move detection
By identity keyA field such as id1 addedYes, unique and presentRoughly linear (hashing)Schema-specific; hides reordering
Sequence alignment (LCS / Myers)Longest run of equal elements1 inserted at [0]NoO(n × m), or O((n + m) × D)Edited + moved items become delete + insert
As a set (ignore order)Equal values anywhere1 addedNoO(n log n) sort or hashingEdits become remove + add; order is lost

No strategy is best everywhere. Index comparison is the most predictable, identity keys give the most readable output when you have a reliable key, and set comparison is right only when order genuinely carries no meaning.

Practical tips for a cleaner JSON array diff

Sort arrays before comparing when order is not meaningful

Normalizing both documents turns an index-based diff into an order-insensitive one. For a top-level array of objects:

jq 'sort_by(.id)' before.json > before.sorted.json
jq 'sort_by(.id)' after.json  > after.sorted.json

For an array nested inside a document, update it in place; -S also sorts object keys, which helps if you will use a text diff afterwards:

jq -S '.users |= sort_by(.id)' before.json > before.sorted.json

After sorting, the example produces a single entry in an index-based diff: added $.users[3] with Margaret's record. For arrays of plain values, use jq 'sort'.

Compare keyed maps instead of arrays

If elements have a unique id, converting the array to an object gives you identity matching in any tool that diffs objects. In jq 1.6 or later:

jq '.users |= INDEX(.id)' before.json > before.keyed.json

This produces {"users": {"1": {...}, "2": {...}, "3": {...}}}, and the diff path for the new user becomes $.users["4"]. The same idea in Python:

import json

def by_id(path):
    with open(path) as f:
        return {u["id"]: u for u in json.load(f)["users"]}

old, new = by_id("before.json"), by_id("after.json")
print("added:", sorted(new.keys() - old.keys()))
print("removed:", sorted(old.keys() - new.keys()))
print("changed:", sorted(k for k in old.keys() & new.keys() if old[k] != new[k]))

Check for duplicate ids first; with either approach, a later element with the same id overwrites an earlier one.

Append rather than prepend in fixtures

If you control the data, such as test fixtures, snapshot files or seed data, add new items at the end. Appends produce exactly one "added" entry in an index-based diff, so reviews stay short and unrelated entries do not churn.

Try it

Paste the two documents from this article into JSON Mirror to see index-based comparison in action, then sort both by id with jq and compare again to watch seven entries collapse to one. Everything runs in your browser tab, and nothing you paste is uploaded. If your arrays are collections rather than sequences, normalize them first; if order matters, a position-based diff tells you exactly what is at each index.

Frequently asked questions

How do I compare JSON arrays while ignoring order?

Sort both arrays the same way before you compare them. For arrays of objects, sort by a stable field, for example jq 'sort_by(.id)'; for arrays of plain values, jq 'sort' is enough. After sorting, a position-based diff only reports real additions, removals and edits.

Why does my JSON diff say every array item changed when I only added one?

The tool is comparing arrays by index. Adding an item at the start shifts everything after it by one position, so each position is now compared against a different item. Appending to the end, or sorting both arrays by a key first, avoids the cascade.

How do I diff arrays of objects by id?

Turn each array into an object keyed by id and compare the objects instead. In jq 1.6 or later, '.items |= INDEX(.id)' does this; in Python, a dict comprehension such as {x['id']: x for x in items} works. The ids must be present and unique, or entries will be lost.

Does JSON Mirror detect moved or reordered array items?

No. JSON Mirror compares arrays strictly by position, so a moved item shows up as changes at every position it affected. If order is not meaningful in your data, sort both documents by a key before pasting them in.

Is array order significant in JSON?

Yes, as far as the format is concerned. RFC 8259 defines an array as an ordered sequence of values, while object members are unordered. Whether order matters for your data is an application decision, which is why diff tools offer different strategies.