Change one field in a JSON object, then reorder the surrounding array. A diff can suddenly report that nearly every record changed.
Many JSON diff tools compare arrays by position. That is exactly right for arrays whose order carries meaning. But when an array represents a collection of records, positional matching can produce a surprising amount of noise.
Consider these two payloads:
Original
{
"users": [
{ "userId": 101, "name": "Alice", "role": "engineer", "status": "active" },
{ "userId": 102, "name": "Bob", "role": "engineer", "status": "active" },
{ "userId": 103, "name": "Carol", "role": "manager", "status": "active" }
]
}
Changed
{
"users": [
{ "userId": 103, "name": "Carol", "role": "manager", "status": "active" },
{ "userId": 101, "name": "Alice", "role": "engineer", "status": "active" },
{ "userId": 102, "name": "Bob", "role": "engineer", "status": "inactive" },
{ "userId": 104, "name": "David", "role": "designer", "status": "active" }
]
}
A positional comparison pairs the first object on the left with the first object on the right, then the second with the second, and so on. Since Carol moved to the beginning, several unchanged users appear to have been modified.
But a person looking at this data would probably describe only two meaningful changes:
- Bob's status changed from
activetoinactive. - David was added.
The records didn't lose their identity just because their positions changed.

I built JSON Semantic Diff to explore a different approach: try to identify corresponding records before comparing their fields.
The real problem is correspondence
Once two objects have been paired correctly, comparing their fields is straightforward. The difficult question is which object on the left corresponds to which on the right.
It is tempting to solve this by looking for a field named id. Real payloads rarely make it that simple.
An identity might be called:
iduserIdorderNumberskuemail- something specific to the domain
And sometimes no single field is enough.
Field names are also not proof. A field called id can be missing, duplicated, regenerated between responses, or only unique in the tiny sample currently being compared. Conversely, an unfamiliar field may be the most reliable identity in the data.
So JSON Semantic Diff treats a promising name as a hint, not a rule.
Trying to infer identity from the data
For each array of objects, the matcher examines individual fields and field combinations as possible identities. It evaluates each candidate using measurable signals:
| Signal | What it asks |
|---|---|
| Uniqueness | Does the candidate distinguish records within each input? |
| Match coverage | How many records can be paired across the two inputs? |
| Population overlap | Does the overlap represent enough of both arrays? |
| Completeness | Is the candidate present on the records being compared? |
| Type consistency | Are its values represented consistently? |
| Name hint | Does the field name weakly suggest identity? |
These signals answer different questions. A candidate can be unique but useless if none of its values appear in the other document. It can have matching values but cover only a tiny fraction of a much larger array. A familiar field name can help break a close tie, but it should not rescue an otherwise weak candidate.
The signals are combined into a candidate score. There is also a small complexity penalty for composite candidates, so a single reliable field is preferred to an unnecessarily complex combination.

A high score alone is not enough. Automatic matching must also pass safety gates for uniqueness, cross-input coverage, and the margin over the second-best candidate.
That last check matters. If two identities receive nearly identical scores, choosing either one may be arbitrary. A matcher that confidently makes the wrong pairing is worse than a positional diff because its output looks clean while hiding an incorrect assumption.
Some records need a composite identity
Inventory data is a good example. Imagine records shaped like this:
{
"inventory": [
{ "store": "BOS", "sku": "SKU-1001", "quantity": 24, "price": 12.99 },
{ "store": "NYC", "sku": "SKU-1001", "quantity": 13, "price": 12.99 },
{ "store": "NYC", "sku": "SKU-2004", "quantity": 11, "price": 8.50 }
]
}
store is not unique because each store carries many products. sku is not unique because the same product exists in many stores. Together, however, sku + store can identify one inventory record.
After records are paired by that composite identity, the diff can distinguish between:
- an existing product whose quantity changed,
- an existing product whose price changed,
- a newly added store-product combination, and
- records that only moved to another array position.

Composite inference expands the search space and increases the chance of finding accidentally unique combinations. That is why candidate complexity is penalized and why coverage and population overlap remain important.
When inference is ambiguous
Not every array contains a reliable identity.
Values may be duplicated. Too few records may overlap. A field may be missing on part of the array. Two candidates may be almost equally plausible. Some arrays are genuinely ordered sequences and should be compared by position.
When the evidence is not strong enough, JSON Semantic Diff falls back to positional matching.
You can then manually select one or more identity fields when domain knowledge provides information the payload alone cannot establish. Automatic inference should save work when the evidence is strong, without taking control away when it is not.
Deterministic and explainable by design
No AI model or fuzzy semantic matching is involved. Given the same inputs and configuration, the matcher produces the same decision.
The interface exposes the selected candidate, its score, the component signals, the runner-up, and the margin between them. That makes it possible to ask not only what did the matcher choose? but why did it choose this?
The comparison also runs locally in the browser. The JSON does not need to be uploaded to a server, which is useful when inspecting internal API responses, configuration, test fixtures, or production-shaped data.
A cleaner diff needs a cautious matcher
A semantic diff is not merely a prettier rendering of a positional diff. It makes an additional claim about correspondence:
These two records represent the same entity, even though they occupy different positions.
That claim can remove a large amount of noise, but only when it is well supported. The goal is not to infer an identity as often as possible. It is to recognize when the evidence is strong enough to trust one, and to make uncertainty obvious when it is not.
JSON Semantic Diff is still in alpha, and real-world payloads will expose cases that clean examples do not. If you have an array that it matches incorrectly, or one it refuses to match when it should, I would especially like to see the smallest reproducible example.
The project is open source on GitHub.
Top comments (1)
The identity-inference angle resonates - we hit the same wall diffing browser session payloads for automation. Positional diff on a record array is fine until a background job re-sorts the collection server-side and your 'change' is forty modified records that are all just permutations.
Where I'd push: what's the fallback when there's no guaranteed-unique key inside the objects? A third of the arrays we see have candidates that are probably unique (email, slug, SKU) but not guaranteed, and silently guessing wrong is worse than a noisy diff - at least noisy is honest. Does the caller declare the key, or do you auto-detect with a confidence check?