Case study 00
When the spec is too big to generate from
A subsystem's OpenAPI spec was so large and self-contradictory that generating a client from it became a runtime liability, so I hand-built the types instead.
The generated client compiled, which was the cruel part. I had pointed oapi-codegen at one managed subsystem’s OpenAPI document, all 52,000 lines and something like 500,000 tokens of it, and out came a few hundred kilobytes of Go that built on the first try. Green across the board. I shipped it behind a flag and went to lunch pleased with myself, the kind of engineer who generates instead of types.
It broke in production a few days later, on an endpoint I had never personally called. The spec promised that a particular field was an object. The API, in one branch of one operation, returned a bare string. The deserializer did exactly what the schema told it to do and rejected the response, so a request that should have succeeded handed an error back to a caller who had done nothing wrong. Generated code fails like that: downstream and days later, never at the keyboard where you ran the generator.
So I read the spec the way you read a hostile contract, and the contradictions were not rare. An enum listing a value the API never emits. A field marked required that is absent in roughly half of real responses. Two operations sharing one model, each populating it differently, so no single struct is correct for both. At 52,000 lines none of this is visible to a human, and the generator cannot see it either. It transcribes the spec faithfully, and a faithful transcription of a wrong document is a wrong client.
For that subsystem I stopped generating. I keep a notes file instead. It holds the operation signatures I actually call, copied verbatim from the docs, nothing inferred and nothing expanded, and from those I hand-write the Go structs for the handful of fields my code touches. Call it 200 lines I have read against 52,000 I never could.
This turned out to be the same thing I already do for the subsystems that never had a fetchable spec at all. The GraphQL endpoint, the Cube semantic layer, the Ray cluster API, the ClickHouse HTTP interface: there is no OpenAPI document to point a generator at, clean or otherwise. For each I save a notes file of verbatim operation signatures and hand-build the types. The 52,000-line subsystem simply joined that group from the other direction, because its spec was technically present and practically unusable.
The idea worth keeping is that a generator’s output is exactly as correct as its input, and a spec’s correctness falls as it grows. You can hold a 200-line spec in your head and trust it. Once a spec reaches 500,000 tokens, it is wrong somewhere with a probability that rounds to one, and you do not learn which somewhere until a request fails in front of a user. So oapi-codegen stays in the toolbox, restricted to specs that are clean and bounded, the ones small enough that a person has actually read all of them.
The bill for this comes later and quietly. My hand-built structs have no link back to the upstream API. When that team adds a field, renames one, or changes a type, nothing in my build turns red. The generated client at least failed loudly at runtime against a freshly pulled spec; the notes file fails by sitting unchanged while the real API moves out from under it, until a value that used to be populated comes back null and someone opens a ticket weeks later. The only thing keeping the structs honest is me rereading the upstream changelog by hand, once per change I happen to notice, for as long as the code lives.