Building JSON Semantic Diff: Comparing JSON by Meaning, Not Just Position
Most JSON diff tools are perfectly fine - until arrays get involved.

I ran into this while comparing real-world payloads where the data was essentially the same, but the order of records had changed. A background process might sort an array differently, an API might return records in another order, or a new item might get inserted near the top.
Suddenly, what should have been one meaningful change looked like dozens of modifications.
That frustration eventually became JSON Semantic Diff, an open-source JSON comparison tool that tries to understand which objects represent the same entity before deciding what actually changed.
It is still early, and I am actively building it, but the problem turned out to be much more interesting than I initially expected.
The problem with positional array diffing
{
"users": [
{
"userId": 101,
"name": "Alice",
"status": "active"
},
{
"userId": 102,
"name": "Bob",
"status": "active"
}
]
}
And then:
{
"users": [
{
"userId": 102,
"name": "Bob",
"status": "inactive"
},
{
"userId": 101,
"name": "Alice",
"status": "active"
}
]
}
A positional diff compares:
users[0] ↔ users[0]
users[1] ↔ users[1]
From that perspective, almost everything changed. But as a human, the comparison is obvious. Alice is still Alice. Bob is still Bob.
The array was reordered, and Bob's status changed.
The real problem is not comparing the objects. The harder problem is figuring out which object on the left corresponds to which object on the right. That became the central idea behind JSON Semantic Diff.
Treat array comparison as an identity problem
Instead of assuming that array position defines identity, JSON Semantic Diff tries to infer an identity key from the data.
For the users above, userId is a strong candidate. But real payloads are rarely that convenient.
You might get:
{
"store": "BOS",
"sku": "SKU-1001",
"quantity": 24,
"price": 12.99
}
Neither store nor sku uniquely identifies an inventory record by itself.
The identity is really:
store + sku
So the matching engine can consider composite identities, too. Once those objects are paired correctly, the diff becomes much more useful:
BOS + SKU-1001
quantity: 24 → 19
NYC + SKU-2004
price: 8.75 → 9.25
SEA + SKU-1002
added
Instead of a large block of positional noise, you see the changes that matter.
But automatic matching can be dangerous
This was one of the more important lessons while building the project. Automatically finding a field that looks unique is not enough.
Suppose an array contains email, slug, sku, or some other field that happens to be unique in the current sample. That does not necessarily mean it represents the true identity of the record.
Silently pairing the wrong objects can be worse than returning a noisy positional diff. At least the noisy result is obviously wrong. A confidently incorrect semantic diff can be misleading.
So JSON Semantic Diff tries to be conservative.
Candidate identities are evaluated using signals such as uniqueness, completeness, how many records can be matched across both inputs, population overlap, and type consistency. Field names such as id, uuid, or sku can help, but they are only one signal.
The engine also looks at the difference between the best candidate and the runner-up. If two candidates look almost equally plausible, that matters.
A high candidate score does not automatically mean:
"This is definitely the identity."
Sometimes the right answer is:
"I don't know."
And that is intentional.
Falling back instead of guessing
When the inference is not strong enough, JSON Semantic Diff falls back to positional comparison. The user can then manually define the identity fields.
For example:
Now the tool has explicit domain knowledge:
identity = store + sku
I like this balance.
Automatic inference handles the obvious cases, while manual matching remains available when the user understands the data better than the tool does. The automatic suggestion remains visible as well, so the matching decision is not hidden behind a black box.
Explainability became part of the product
Once I started scoring identity candidates, another question appeared: How do I make users trust the result?
Simply displaying:
Matched by sku
wasn't enough. So the UI exposes why a candidate was chosen. For an array, you can inspect things such as:
Uniqueness
Completeness
Match coverage
Population overlap
Type consistency
Name hint
Candidate score
Runner-up
Margin
The goal is not to force everyone to understand the algorithm. Most users should be able to ignore this completely. But if the tool makes an unexpected matching decision, you should be able to inspect it rather than wondering what happened internally. That is also why the matching algorithm is deterministic.
There are no embeddings, fuzzy models, or LLMs deciding which objects "feel similar." Given the same JSON and the same settings, the result should be the same.
More than array matching
Identity-aware arrays became the main differentiator, but while building the tool, I kept encountering other sources of noisy diffs.
Real JSON often contains timestamps, dynamically generated metadata, numeric values represented as strings, or fields that simply do not matter for a particular comparison.
So the tool now also supports normalization and ignore rules. For example, you might want to ignore:
$.metadata.requestId
or every quantity inside an array:
$.inventory[*].quantity
There are also separate Tree, List, and Source views.
Tree view is useful when the structure itself matters, and you want to navigate changes semantically. List view gives you a list of flattened paths and what changed. Source view is useful when you want something familiar and code-like.
Local by design
JSON payloads often contain things developers don't want uploaded just to compare them. API responses, configuration files, internal identifiers, customer data, debug payloads—the list gets sensitive quickly.
JSON Semantic Diff therefore runs entirely in the browser. Your JSON does not need to be sent to a backend for the comparison to work.
That constraint also influenced the architecture. The core diff engine is plain TypeScript and remains separate from the Angular UI.
The web application is essentially a workbench around that engine.
Building it in the open
The project is still early.
I have been intentionally sharing it before considering it "finished" because identity inference is exactly the sort of feature that needs strange real-world data to improve.
Synthetic examples only get you so far. The interesting cases are things like:
arrays where no field is guaranteed unique
partially populated identifiers
composite identities
additions and removals mixed with reordering
duplicate candidate keys
records whose supposedly stable fields change
wildly different array populations
Those cases are helping shape both the matching algorithm and the UI around it. I would rather have the tool say "I cannot safely determine this" than produce a cleaner-looking but incorrect diff.
Try it
You can use JSON Semantic Diff here:
https://jsonsemanticdiff.dev
The project is open source:
https://github.com/cchandurkar/json-semantic-diff
Also on Product Hunt:
https://www.producthunt.com/products/json-semantic-diff
If you work with JSON regularly, I am especially interested in payloads where the matching behaves unexpectedly.
The easiest way to improve an identity-inference algorithm is to give it data that proves its assumptions wrong.
And at this stage of the project, that is exactly what I am looking for.
