Lesson 16 / 27
What Counts as a Breaking Change
Tell backward-compatible additions from changes that break existing clients, and detect them automatically.
Safe vs breaking
Usually safe (additive): adding a new optional request field, a new response field, a new endpoint, a new optional query parameter, a new enum value in a response only if clients are told to expect unknown values. Breaking: removing or renaming a field, changing a field's type or meaning, making an optional input required, narrowing allowed values, changing status codes or error formats, changing authentication, or changing default behaviour (like default page size or sort order). Clients should follow the tolerant reader principle (ignore fields they do not know) and servers the robustness principle for inputs. Run an automated contract diff (compare the old and new OpenAPI spec) in CI so no breaking change ships by accident.
Change without breaking
Most changes can be additive. When a change must break, a version lets old and new clients coexist.
A breaking-change detector, run
I ran this plain-Python example. Adding an optional nickname field is compatible. The second change set is flagged on four counts: id changed from int to string, an enum value was removed, name was removed, and a new required field appeared.
import hmac, hashlib, json, time, bisect
def breaking_changes(old, new):
out = []
for name, spec in old["fields"].items():
if name not in new["fields"]: out.append(f"removed field '{name}'")
else:
n = new["fields"][name]
if n["type"] != spec["type"]: out.append(f"type of '{name}' changed {spec['type']} -> {n['type']}")
if set(spec.get("enum", [])) - set(n.get("enum", spec.get("enum", []))): out.append(f"enum value removed from '{name}'")
for name, spec in new["fields"].items():
if name not in old["fields"] and spec.get("required"): out.append(f"new required field '{name}'")
return out or ["compatible"]
v1 = {"fields": {"id": {"type": "int"}, "plan": {"type": "string", "enum": ["free", "pro"]}, "name": {"type": "string"}}}
v2a = {"fields": {**v1["fields"], "nickname": {"type": "string"}}}
v2b = {"fields": {"id": {"type": "string"}, "plan": {"type": "string", "enum": ["pro"]}, "tier": {"type": "string", "required": True}}}
print(breaking_changes(v1, v2a)); print(breaking_changes(v1, v2b))
Output:
['compatible'] ["type of 'id' changed int -> string", "enum value removed from 'plan'", "removed field 'name'", "new required field 'tier'"]
Treat meaning as part of the contract
Changing "amount" from rupees to paise keeps the type but breaks every client. Behaviour and units are contract too, so review them in changes just like field names.
Quick check: Which change is breaking?
- Adding a new endpoint
- Adding a new optional response field
- Removing a field from a response
- Adding an optional query parameter
Answer
Removing a field from a response — Existing clients may read the removed field, so its removal breaks them.