The outages that hurt are rarely the ones where the API returns 500. Those page someone
immediately. The expensive failures are the ones where every endpoint returns 200 OK
and the data underneath quietly changed shape.
How this happens
Nobody sets out to break a contract. It happens as a side effect:
- A serializer gains a field, and an ORM change makes an existing one nullable.
- An ID grows past what a 32-bit integer holds and is returned as a string "to be safe".
- An empty relation starts returning
nullinstead of[]after a library upgrade. - A timestamp switches from Unix seconds to ISO 8601 during a refactor.
Every one of those ships a 200. Every status-code assertion passes. The consumer
crashes at runtime, usually somewhere far from the actual cause.
Why tests miss it
Most API tests assert two things: the status code, and one or two fields the author happened to care about.
assert response.status_code == 200
assert response.json()["id"] == 42
That test passes when email disappears entirely. It passes when teams becomes
null. It is checking that the endpoint responded, not that the response is usable.
Assert the shape, not a sample
The fix is to state the whole expected structure once and check responses against it: which fields must be present, what type each is, and — the two that catch the most real bugs — whether a field is required and whether it is nullable.
"Sometimes missing" and "sometimes null" are different failures and both need to be explicit. A schema forces you to decide, which is most of the value even before it catches anything.
Where to point it
- Third-party APIs. You do not control their release schedule and will not get a changelog entry for a nullability change.
- Your own API, before release. Validate the live endpoint against its own OpenAPI spec. The two disagreeing is common enough to be worth checking every time.
- Webhook payloads. See why your webhook works in production but not locally.
The practical walkthrough is in validating API responses against a schema.