Diff two API responses without the noise
You changed a serializer, bumped a dependency, or moved an endpoint to a new service. Now you want to know one thing: did the response change?
The usual move is to save both responses and run diff before.json after.json. That works until it doesn't.
Why text diffs lie about JSON
Take this response before and after a refactor:
{ "id": 42, "name": "Alice", "role": "admin",
"tags": ["beta", "staff"],
"plan": { "tier": "pro", "seats": 5 } }
{ "id": 42, "name": "Alice", "roles": ["admin", "editor"],
"tags": ["staff", "beta"],
"plan": { "tier": "pro", "seats": "5" } }
A line diff has three problems here:
- Formatting noise. If one side is minified or the keys come out in a different order, every line "changed".
- Reordered arrays look like edits.
["beta", "staff"]vs["staff", "beta"]is usually the same set of tags, but it shows up as two changed lines. - Type changes are nearly invisible.
5vs"5"is two characters. In a 400-line response you will scroll right past it, and it's the one that breaks clients.
Diff the structure, not the text
A structural diff parses both sides and compares values by path. Running the two responses above through one gives:
| Path | Before | After | Change |
|---|---|---|---|
$.role |
"admin" |
(missing) | removed |
$.roles |
(missing) | ["admin","editor"] |
added |
$.tags[0] |
"beta" |
"staff" |
changed |
$.tags[1] |
"staff" |
"beta" |
changed |
$.plan.seats |
5 |
"5" |
changed |
Whitespace and key order are gone from the picture. But the tags rows are still noise.
Turn on order-insensitive arrays
Compare arrays as sets instead of by index and the same diff becomes:
| Path | Before | After | Change |
|---|---|---|---|
$.role |
"admin" |
(missing) | removed |
$.roles |
(missing) | ["admin","editor"] |
added |
$.plan.seats |
5 |
"5" |
changed |
Three rows, and every one is real:
rolewas renamed torolesand became an array. Breaking change for every client readingrole.seatswent from number to string. Breaking change for anything doing math on it.
For arrays of objects (say, a list of line items), matching elements by their id instead of by position gives the same benefit: inserting one item at the top doesn't mark every following item as changed.
Doing it in the browser
I built this into Formaty's JSON Diff: paste the old response on the left, the new one on the right, and toggle order-insensitive comparison (arrays of objects are then matched by id). It shows side-by-side or inline highlighting plus the path-level change list. Everything runs locally, so production payloads never leave your machine.
If you'd rather script it, the same idea works in a test: parse both sides, walk them by path, and treat arrays as multisets unless order matters for that field.
FAQ
Does key order matter? No. Objects are compared by key, so {"a":1,"b":2} equals {"b":2,"a":1}.
Can I export the result? Yes. Copy or download the change list as a JSON report to attach to a PR or ticket.
Formaty is free and open source (GitHub). 60-second demo.
About the author
- I design distributed systems that operate under real-world pressure, not just in ideal conditions. Over the last 10+ years, I have built backend systems across FinTech, Identity and Access Management, Immigration Tech, and SaaS platforms, including real-time pipelines handling 5K+ events/sec and high-throughput services supporting critical workflows. At Deel, I work on immigration systems that power global workforce mobility. I build and scale services that integrate with complex third-party systems and government platforms. Previously at JumpCloud, I built systems for an Identity and Device Management platform, including real-time monitoring, alerting, and event-driven pipelines at scale. I also work with AI agents, building agentic workflows and LLM-backed automation on top of these systems. I treat an agent like any other distributed component: define the boundaries, handle failure, make it observable. My view is that you cannot direct an agent well if you have never built the thing yourself. What I work on • Distributed systems and microservices • Event-driven architectures using Kafka and NATS • Real-time processing and alerting systems • AI agents, agentic workflows, and LLM integrations • Cloud-native systems on AWS using Docker and Kubernetes • Observability with Datadog, logging, tracing, and alerting Core technologies • Go, Node.js, TypeScript • Kafka, NATS • PostgreSQL, MongoDB, Redis • gRPC, REST APIs • Docker, Kubernetes, AWS • LLM APIs, MCP, agent frameworks How I think about systems • Reliability over hype • Systems fail at scale, so design for failure first • Observability is a core requirement, not an afterthought • Good engineering is about trade-offs, not tools I focus on building systems that behave predictably under load, evolve over time, and solve real business problems. Always open to discussions on system design, scaling challenges, backend architecture, and AI agents.
Comments
Join the discussionNo comments yet. Be the first to comment.
