For a JSON API response, a breaking change is one that makes existing consumer code return a wrong result, throw, or misbehave, without the consumer changing anything. Some changes that look alarming don't qualify. Some small ones do.
Changes that break consumers
- A field is removed. Code that reads it gets
undefinedand keeps going. You could argue that's worse than an exception, because the failure surfaces somewhere downstream (a blank template, a calculation that quietly becomesNaN) and not where the change was made. - A field changes type.
{"count": 5}turning into{"count": "5"}is the textbook case. The+operator adds numbers and concatenates strings, so this usually doesn't throw. It computes the wrong answer. - A field that was always there becomes sometimes absent. This is the hardest to spot. Any single response looks fine. It breaks because consumer code assumed the field was
requiredand never checked for null. Comparing one before-response to one after-response won't show it. You have to see the field missing on some requests. - An enum-like string gets a value the consumer doesn't handle.
"status": "pending" | "complete"picking up a third value ("cancelled") is neither a type change nor a removal. The old code has no branch for it, so it usually falls through to a default case without any error.
Changes that usually don't break anything
- A new optional field next to the existing ones. Well-written consumers ignore fields they don't know.
- Reordered object keys. Key order has no meaning in JSON (the JSON Diff Viewer's guide covers this).
- Whitespace or formatting changes in the raw body. They disappear once the body is parsed.
- Reordered array elements, if the consumer doesn't rely on order. That depends on the array. One that is implicitly sorted by priority or timestamp can break logic even though its shape is unchanged.
Triage: additive, subtractive, or type-changing
Putting a change in one of three buckets tells you how fast to look at it, before you know whether anything downstream will break:
- Additive: a new field, a new enum value, a new optional parameter. Low severity by default, because consumers that only read fields they know about don't notice. The risk sits with consumers that assume a closed world, like a schema validator with
additionalProperties: falseor a switch with no default case. An additive change only hurts a consumer that has chosen to reject anything unfamiliar. - Subtractive: a field removed, or a field that was always present now sometimes missing. High severity. Every consumer that reads the field is affected, and the failure tends to be an
undefinedmoving through a calculation, not a thrown error. Give these a deprecation window instead of removing them without notice. - Type-changing: the field name stays, the value's type changes (number to string, single value to array). Also high severity, and harder to catch by reading the response, because the field is still there with a plausible value. The bug lives in what consumer code does with that value. Someone scanning the JSON to see whether it looks right will miss it. A type-aware diff won't.
Semver won't catch response-shape drift
Semantic versioning describes a package's or contract's declared interface. A major version bump says "we changed something on purpose and we're calling it breaking." It says nothing about whether the implementation still matches that interface on every request.
Response drift is nearly always an accident from the producer's side. A refactor changes what a database query returns. A conditional path stops setting a field for some combination of inputs. A third-party service the API proxies starts returning a new value. None of these feel like a decision to make a breaking change, so none of them trigger a version bump. Nobody thought they were touching the contract. Semver covers intentional, documented changes and has no way to catch accidental ones. That's why drift has to be found by diffing real responses, not assumed safe because the version number stayed put.
Two cases the rubric doesn't sort cleanly
A field going from optional to always present looks harmless under the additive/subtractive split, since nothing that used to be there is missing. It can still break a test fixture or mock that left the field out on purpose to exercise the "missing" path. Production code is fine. The test suite breaks.
The other is null versus an absent key. {"note": null} and {} (no note key) mean different things in JSON, but several languages' JSON libraries turn both into the same empty value by default. So a producer that switches from always sending null to sometimes leaving the key out can look identical from inside consumer code, until something checks 'note' in response instead of checking the value.
One-off diffs vs. drift over time
These are separate jobs, and this site has a tool for each. The JSON Diff Viewer is for when you already have both sides: a response from before a deploy and one from after, or two environments you're trying to reconcile. It keeps nothing between visits. You paste two payloads and get a diff.
Live Types is for when you don't yet know something changed. You save a snapshot of a response's inferred shape while it's known to be good. Later, when you paste a fresh sample from the same endpoint, it diffs the new shape against the saved one and lists the fields that were added, removed, or changed type.
This is how the sometimes-absent field from earlier shows up. Pasting several fresh samples over time reveals a field that's only missing some of the time. A single before/after diff can't, because it only sees one sample on each side.
What this doesn't automate
Both tools run when you paste something. Neither polls a live endpoint in the background. If you want drift checked on every deploy or on a schedule, you need a CI step that diffs against an exported snapshot, which is separate work from either tool here. The browser tools stay snapshot-and-compare because there's no server behind them to poll anything, and nothing you paste leaves your machine.