Read real OpenAPI YAML fragments, a oasdiff verdict, and a generated client, then pick the highest-leverage fix to keep the contract honest.
APISenior◷ 14 min
Level
FoundationsJuniorMiddleSenior
Contract problems are caught in the spec file, the diff output, and the generated client. Read each fragment, predict what it does to consumers, and choose the fix a senior engineer makes first.
Goal
Practise the loop every API change runs through: read the schema, predict the breaking-change and codegen consequences, and reach for the fix that keeps the spec load-bearing.
Snippet 1 — the loose schema
components: schemas: Order: type: object properties: id: { type: string } items: type: object # untyped blob total: {} # no type at all additionalProperties: true # anything else is allowed # no `required:` array
Quiz
Completed
A generator turns this Order schema into a client. What will the generated type look like, and what is the fix?
Heads-up Generators do not invent constraints the schema omits. An untyped object becomes any, a missing required array makes everything optional, and open additionalProperties forbids a closed type. The output is faithfully loose.
Heads-up This schema is valid OpenAPI — loose, but legal. It generates fine; it just produces a weak, any-riddled client that defeats the point of codegen.
Heads-up There is nothing to validate against — additionalProperties: true and untyped fields accept almost anything. Loose schemas weaken both the generated types and edge validation.
Snippet 2 — the oasdiff verdict
$ oasdiff breaking main.yaml pr.yaml1 breaking changes: 1 error, 0 warningerror api-required-request-property-added in API POST /orders added the required request property 'tenantId'
Quiz
Completed
The PR adds tenantId as a required field on POST /orders and oasdiff reports this in CI. What should the pipeline do, and what is the compatible alternative?
Heads-up Adding an optional field is compatible; adding a required one is the textbook breaking change, which is exactly why oasdiff classifies it as an error, not a warning.
Heads-up Regenerated docs describe the change but do not prevent the breakage. The whole value of the gate is that the breaking change fails the build before any client breaks.
Heads-up The break is on the request, not the response. Touching the response does not make a newly-required request field compatible.
You migrate the spec to 3.1 and rewrite nullability like this. What must you watch for in tooling and diffs?
Heads-up 3.1 removed nullable entirely; it is ignored or invalid. You must use type: [string, 'null'], and tools built for 3.0 may not understand it.
Heads-up Both express 'string or null' — the contract is identical. The migration is syntactic; the allowed values are the same.
Heads-up type: [string, 'null'] is a union of allowed types, unrelated to whether the field is required. Required-ness is controlled only by the required array.
Snippet 4 — code-first annotations
class CreateOrder(BaseModel): id: str items: list[Item] coupon: str | None = None # optional, nullable# FastAPI generates the OpenAPI spec from this model.# A later edit makes coupon required: coupon: str # now required, no default
Quiz
Completed
In a code-first setup, an engineer makes coupon required by removing its default. The generated spec updates automatically. What is the risk, and what closes it?
Heads-up Code-first prevents drift between spec and handler, not between the contract and what clients depend on. A required-field change is breaking regardless of how the spec was produced.
Heads-up The model is valid Python; FastAPI starts fine and faithfully publishes coupon as required. The danger is downstream, on clients, not at startup.
Heads-up Spec-first surfaces the change for review but still does not block it without a diff gate. The fix is enforcement (oasdiff in CI), which both models need equally.
Recap
Every contract change is read in three places: the schema (loose types and open additionalProperties produce any-riddled clients — tighten with explicit types, required arrays, and $ref), the diff (oasdiff classifies adding a required field, removing a response field, or narrowing a type as breaking — fail the PR and migrate compatibly), and the generation direction (code-first stays in sync with code but not with consumers, so it needs the same gate spec-first does). OpenAPI 3.1 moves nullability into the type array and aligns with JSON Schema 2020-12 — verify tool support before you migrate. Read the artifact, predict the consumer impact, then fix at the contract level.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.