skip to content

How do you wire openapi-generator into a build so generated code stays maintainable?

level: seniorimportance: should knowfreq 42%

answer

  1. Treat it like compiler output
  2. Never edit; regeneration wins
  3. Pin the tool, pin the input
  4. Adapter module bounds the churn
  5. Commit or regenerate — decide on purpose

basics

~20 s

Generate into a build output directory, never hand-edit the result, pin the generator version so output does not shift under you, and wrap generated clients behind your own interface. Then decide deliberately whether to commit the output or regenerate it every build.

solid answer

~50 s

Treat generated code the way you treat compiler output. Emit it into a build directory rather than `src`, so nobody is tempted to edit it and regeneration is a clean overwrite; if one file genuinely must be preserved, that is what `.openapi-generator-ignore` is for. **Pin the generator version** — templates change between releases, and an unpinned tool means today's build produces different code from yesterday's for the same spec. Feed it a pinned local spec file, not a live URL. Wrap the generated client in a thin interface of your own so a regeneration that renames a method touches one adapter rather than fifty call sites, and keep generated models out of your domain types. The commit-or-generate decision is a real tradeoff: committing gives reviewable diffs and a build with no extra toolchain; generating each build guarantees freshness. Either way, wire it as a build task — Gradle's `org.openapi.generator` plugin or the Maven plugin — so it cannot be forgotten.

code

bash · 8 lines
bash
# pinned generator, pinned local spec, options in version control
openapi-generator-cli version-manager set 7.6.0

openapi-generator-cli generate \
  -i specs/orders-v1.yaml \
  -g spring \
  -o build/generated/orders-api \
  --additional-properties=interfaceOnly=true

go deeper

for a junior

Know that generated code is build output: it is overwritten on every run, so fixes belong in the spec or your own code, never in the generated files.

for a middle

Explain the reproducibility inputs — pinned generator version, pinned local spec, options in version control — and what .openapi-generator-ignore is for.

for a senior

Argue the commit-versus-regenerate tradeoff for a real project and describe the adapter layer that stops generated types spreading through the codebase.

for a principal

Set the org-wide policy: how SDKs are produced, versioned and published, who owns generator upgrades, and how contract changes are made visible in review across many repositories.

## Generated code is build output, not source The single decision that prevents most pain: generated files live under the build output directory and are on the same footing as compiled classes. They are recreated from an input, they are not reviewed line by line, and they are never edited. When generated code is written into `src` instead, three things follow — somebody eventually fixes a bug by editing it, the next regeneration silently reverts that fix, and from then on nobody trusts the pipeline and regeneration becomes a manual ritual performed nervously. If a specific file truly must survive regeneration, `.openapi-generator-ignore` (same syntax idea as an ignore file) excludes it explicitly, which at least makes the exception visible. ## Pin everything that affects the output The output is a function of three inputs, and all three need pinning. **The generator version.** Templates, naming rules and library defaults change between releases. An unpinned generator means the same spec produces different code on different days and on different machines — the classic "works locally, breaks in CI" for generated projects. Pin it in the build file, and treat a bump as a change to be reviewed like a dependency upgrade, because it is one. **The spec input.** Generate from a file in the repository. Pointing `-i` at a live URL makes your build depend on someone else's uptime, gives you no record of what you built against, and lets a remote edit change your code with no diff anywhere. Vendor third-party specs and update them deliberately. **The options.** Keep `--additional-properties` and any custom templates in the build configuration under version control, never in someone's shell history. If you fork templates, that fork is now code you maintain across generator upgrades — a real cost, so fork only when configuration cannot do the job. ## Commit the output, or regenerate every build? Both are defensible; what matters is choosing on purpose. *Committing* the generated code gives you a visible diff whenever the contract changes, keeps the build simple for anyone without the generator toolchain installed, and makes IDE navigation trivial. The cost is repository noise and the risk of a stale commit if someone changes the spec and forgets to regenerate — mitigated by a CI step that regenerates and fails on any difference. *Generating every build* guarantees freshness and keeps the repository clean, at the price of a toolchain dependency (often a JVM plus the generator), slower cold builds, and contract changes that are invisible in review because no file changed. A reasonable default: generate at build time for internal services where the spec lives in the same repository, and commit the output for published SDKs where consumers want to read the code and the diff is the release note. ## Contain the blast radius Generated clients are not neutral. Their models carry serialisation annotations, their nullability reflects the spec's `required`/`nullable` accuracy, and their method names change when the spec's `operationId`s or tags change. Let those types spread through your codebase and every regeneration becomes a wide refactor. The containment pattern is a thin adapter: one small module that depends on the generated client, exposes your own interface in your own domain types, and maps between them. Regeneration then breaks at most that module. It also means a decision to switch generators, or to hand-write the client, is a local change. The same applies on the server side. Implement the generated interface in a controller that immediately delegates to your service layer, so generated request/response models never reach your domain logic. ## Make the contract change visible Whatever the layout, the change you must not miss is a contract change. Wire generation into the normal build task graph so a compile failure appears the moment the implementation stops satisfying the spec — that compile error is the feature. Add a CI job that regenerates from the committed spec, and if you commit the output, fail on any difference. And validate or lint the spec before generating, because a generator will happily produce plausible garbage from a document that a linter would have rejected in a second.

  • Why pin the openapi-generator version rather than tracking the latest?
    Because output is a function of the tool's templates and naming rules, which change between releases. An unpinned generator makes the same spec produce different code on different machines and different days, breaking reproducibility. Treat a version bump as a reviewed dependency upgrade and regenerate deliberately so the diff is visible.
  • A teammate fixed a bug by editing a generated file. What do you do?
    Revert it and fix the cause upstream — in the spec, in a generator option, or in the adapter layer that wraps the generated code. Edits to generated files vanish at the next regeneration, and the moment one exists people stop regenerating. If a file genuinely must be preserved, list it in `.openapi-generator-ignore` so the exception is explicit.
  • Why wrap a generated client behind your own interface?
    Because its method names follow the spec's `operationId`s and tags, and its models carry generator-specific annotations and nullability. Letting those types spread means every regeneration becomes a wide refactor. A thin adapter exposing your own domain types confines the churn to one module and keeps switching generators a local change.

saying these in an interview costs you the question

  • Generates into src and hand-edits the result
  • Runs the generator unpinned or ad hoc from a shell
  • Generates directly from a live third-party URL
  • Uses generated models as the domain model
  • Never regenerates in CI, so the committed output goes stale

context