Skip to main content

Command Palette

Search for a command to run...

Building JSON Semantic Diff: Comparing JSON by Meaning, Not Just Position

Most JSON diff tools are perfectly fine - until arrays get involved.

Updated
7 min readView as Markdown
Building JSON Semantic Diff: Comparing JSON by Meaning, Not Just Position
C
I’m a software engineer who enjoys building things that solve real problems - especially the kind of tools I wish already existed. Most of my work sits somewhere between backend systems, data, developer tooling, and product engineering. Outside of my day job, I like experimenting with small products, open-source projects, and ideas that start as “what if?” and slowly turn into something usable.

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.

Building: JSON Semantic Diff

Part 1 of 1

A behind-the-scenes series on building JSON Semantic Diff — from the matching algorithm and identity inference to UX decisions, edge cases, open-source lessons, and everything involved in turning a useful idea into a real developer tool.