OpenAPI Generator: SDK Generation Setup & CI Automation
OpenAPI Generator is the free, Apache-2.0, Java-based CLI that turns an OpenAPI spec into client SDKs across more than fifty languages, and it is the workhorse of any SDK Generation & Changelog Automation pipeline that needs broad language coverage without per-SDK cost. This guide covers the openapi-generator-cli v7.x toolchain end to end: installing it through the npm wrapper, pinning the generator version in openapitools.json, driving output with --additional-properties, wiring it into CI, and overriding templates when the default output is not idiomatic enough.
The scope here is the generator itself — configuration, CI integration, customization, and troubleshooting. It does not cover the managed alternatives Fern and Speakeasy, which trade control for hand-tuned output and built-in publishing. If your goal is the widest possible language matrix with everything running in your own CI, this is the right tool.
Two facts shape everything that follows. First, the project distributes the actual code generator as a single Java JAR, and the npm package you install is a thin wrapper that downloads and runs that JAR. Keeping those two versions distinct in your head prevents most version-related confusion. Second, the generator is template-driven: every line it emits comes from a Mustache template, which is what makes the output both predictable and fully overridable. Once you internalize “spec plus templates plus properties produces code,” the configuration surface stops feeling sprawling and starts feeling like three knobs.
Prerequisites & Environment Setup
OpenAPI Generator runs on the JVM, so a Java runtime is mandatory even when you invoke it through npm. Install Java 11 or newer — v7.x requires Java 11 as the floor and works on 17 and 21:
java -version # must report 11+ ; v7.x will not run on Java 8
Use the npm wrapper @openapitools/openapi-generator-cli rather than downloading the JAR by hand. The wrapper manages which generator version runs and reads openapitools.json, which keeps local and CI invocations identical:
# Install the wrapper as a dev dependency (pin the wrapper too).
npm install --save-dev @openapitools/[email protected]
The wrapper and the generator are two separate versions. The npm package above (2.13.x) is the CLI wrapper; the generator JAR it downloads (7.7.0 here) is what actually produces code. Set the generator version explicitly so the wrapper does not silently pull a newer one:
npx @openapitools/openapi-generator-cli version-manager set 7.7.0
That command writes the version into openapitools.json. Commit that file. Confirm the active version before generating anything:
npx @openapitools/openapi-generator-cli version
# 7.7.0
Pin the npm wrapper in package.json with an exact version rather than a caret range. A caret like ^2.13.4 lets npm ci resolve a newer minor wrapper that may change defaults or download behavior, which undermines the reproducibility the generator pin gives you. Use an exact version so both halves of the version story are locked:
{
"devDependencies": {
"@openapitools/openapi-generator-cli": "2.13.4"
}
}
If your organization blocks the wrapper from downloading the JAR at runtime — common in locked-down CI — set OPENAPI_GENERATOR_DOWNLOAD_BASE_URL to an internal mirror, or pre-stage the JAR and point the wrapper at it. The wrapper caches the JAR under node_modules, so a warm cache means generation does not hit the network at all, which is the configuration you want for deterministic, offline-capable CI.
Core Configuration
openapitools.json is the single configuration file the wrapper reads. It pins the generator version and can hold per-generator settings so CI runs need no long command lines:
{
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.7.0",
"generators": {
"typescript": {
"generatorName": "typescript-axios",
"inputSpec": "openapi.yaml",
"output": "sdks/typescript",
"additionalProperties": {
"npmName": "@acme/api-client",
"supportsES6": true,
"withInterfaces": true,
"useSingleRequestParameter": true
}
}
}
}
}
Each key earns its place. version pins the generator JAR. generatorName selects the template set — typescript-axios here; run npx @openapitools/openapi-generator-cli list to see all names. inputSpec and output remove the need for -i/-o flags. The additionalProperties block is generator-specific: npmName sets the package name, supportsES6 emits modern syntax, withInterfaces generates separate interface files, and useSingleRequestParameter bundles operation arguments into one object so call sites stay stable as parameters are added.
With a named generator block, the entire invocation collapses to:
# Reads openapitools.json; no -i/-g/-o needed.
npx @openapitools/openapi-generator-cli generate
For ad-hoc runs or generators you do not keep in the config, pass --additional-properties on the command line as comma-separated key=value pairs:
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml -g python -o sdks/python \
--additional-properties=packageName=acme_api_client,projectName=acme-api-client,library=urllib3
Discover every property a generator accepts before guessing — each generator documents its own set:
npx @openapitools/openapi-generator-cli config-help -g typescript-axios
For settings that are long or that you want under review, use a config file instead of inline properties. A -c config file holds the same additionalProperties plus generator options as YAML or JSON, which keeps diffs clean when a property changes:
# config/typescript.yaml — passed with -c config/typescript.yaml
npmName: "@acme/api-client"
npmVersion: "2.4.0"
supportsES6: true
withInterfaces: true
useSingleRequestParameter: true
enumPropertyNaming: "UPPERCASE" # stable enum member names across regenerations
modelPropertyNaming: "camelCase" # idiomatic TS field names from snake_case spec
# A config file and openapitools.json can coexist; explicit -c wins for that run.
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml -g typescript-axios -o sdks/typescript -c config/typescript.yaml
Prefer one configuration source per generator so there is no ambiguity about which value applies. Putting the generator block in openapitools.json is the cleanest default because the wrapper reads it automatically; reach for a separate -c file only when a property set is large enough to deserve its own reviewable file.
Integration Pattern
Wire generation into CI so a spec change regenerates the SDK deterministically. The workflow below lints the spec, regenerates the TypeScript client from the committed openapitools.json, and fails if the generated output differs from what is committed — a guard that proves the checked-in SDK matches the spec. This slots into the larger release pipeline described in SDK Generation & Changelog Automation.
# .github/workflows/generate-sdk.yml
name: Generate and verify SDK
on:
pull_request:
paths: ['openapi.yaml', 'openapitools.json', 'templates/**']
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- uses: actions/setup-java@v4 # generator runs on the JVM
with:
distribution: 'temurin'
java-version: '17'
- run: npm ci
- name: Lint spec before generating
run: npx @redocly/cli@1 lint openapi.yaml --max-problems 0
- name: Generate SDK from openapitools.json
run: npx @openapitools/openapi-generator-cli generate
- name: Fail if generated output drifts from committed SDK
run: |
if ! git diff --exit-code -- sdks/typescript; then
echo "Generated SDK differs from committed code. Run generation locally and commit." >&2
exit 1
fi
The setup-java step is mandatory; without a JVM the generator aborts with a command not found style failure on the JAR. The drift check makes the committed SDK trustworthy: reviewers see the real diff a spec change produces, and nobody can hand-edit the client without CI noticing.
There is a deliberate choice embedded in this workflow: it commits the generated SDK to the repository and verifies it, rather than generating ephemerally and publishing. Committing the output trades a larger diff for full reviewability — a teammate can read exactly what a spec change does to the client before it ships, and the .openapi-generator-ignore file lets hand-maintained additions live alongside the generated code. If you instead generate and publish in one shot without committing, drop the drift check and gate on the SDK building cleanly instead. Pick one model and keep it consistent across languages so contributors are not guessing which SDKs are checked in.
For the publish half of the pipeline — computing the version bump from a spec diff and pushing to npm, PyPI, and Go registries — see the release workflow in SDK Generation & Changelog Automation, which uses this generator as its code-emitting stage. Keeping generation (this workflow) and publishing (the parent pipeline) as separate jobs means a generation failure never leaves a half-published release behind.
Advanced Options
Custom templates with -t. When the default output is not idiomatic enough, override individual Mustache templates instead of the whole generator. Extract the defaults, edit only the files you need, and point -t at your directory:
# Pull the default templates for the generator you use.
npx @openapitools/openapi-generator-cli author template -g typescript-axios -o templates/typescript-axios
# Generate using your overrides; un-overridden templates fall back to the built-ins.
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml -g typescript-axios -o sdks/typescript \
-t templates/typescript-axios
Keep only the templates you actually changed in your directory — every file you leave there is one you must maintain against future generator updates. The full workflow, including which templates control which output, is in customizing OpenAPI Generator templates.
When you bump the generator version, re-extract the default templates into a scratch directory and diff them against your overrides. Upstream template fixes — a corrected null check, a new import — will not reach your output while you shadow that template, so a periodic three-way reconciliation is the cost of overriding. Override the smallest unit possible: prefer overriding partial_header.mustache over the entire api.mustache if you only need to change a file header, because the smaller the override, the less upstream improvement you forfeit.
Protecting files with .openapi-generator-ignore. The generator overwrites every file it owns on each run. To keep hand-written files — a custom README, an auth helper, CI config — list them in .openapi-generator-ignore in the output directory. The syntax mirrors .gitignore:
# sdks/typescript/.openapi-generator-ignore
README.md
src/auth/customTokenProvider.ts
.github/**
The generator writes this file once if it is absent, then never touches it, so it is safe to edit. Use negation (!path) to re-include a file inside an ignored directory when you want to protect a folder but still let one generated file through. Treat the ignore file as the documented boundary between generated and hand-maintained code: anything listed is yours to own, everything else is the generator’s, and that contract is what keeps regeneration safe.
Global properties to scope generation. Use --global-property to limit what gets generated — useful when you only want models, or want to skip tests:
# Generate only model classes, skip APIs and supporting files.
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml -g python -o sdks/python \
--global-property=models,modelTests=false,modelDocs=false
This is distinct from --additional-properties: global properties control which artifacts the generator emits, while additional properties control how a given generator shapes them.
Stable operation grouping with --api-name-suffix and tags. Generated API classes are grouped by the spec’s first tag on each operation, so an operation tagged Orders lands in OrdersApi. If your spec uses inconsistent tags, the generated class layout shifts every time tags change, which churns the public surface of the SDK. Normalize tags in the spec — one primary tag per operation, named after the resource — before relying on the generated grouping. This is a spec-hygiene fix, not a generator flag, and it pays off across every language because the grouping rule is shared:
# Inspect how operations will group before generating.
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml -g typescript-axios -o /tmp/preview \
--dry-run
The --dry-run flag reports which files would be written without writing them, so you can confirm the class layout and catch an accidental ungrouped operation before it reaches a committed SDK.
Verification & Testing
Confirm three things after every generation: the output compiles, it matches the spec, and polymorphic types resolved correctly. Start by building the generated package in its native toolchain — a TypeScript client must tsc cleanly:
cd sdks/typescript && npm ci && npm run build
# tsc exits 0 with no errors when the generated types are sound
Validate the spec separately so a generation failure is never mistaken for a spec problem:
npx @openapitools/openapi-generator-cli validate -i openapi.yaml
# Validating spec (openapi.yaml)... No validation issues detected.
Inspect the generated model files for any oneOf/anyOf schema to confirm the discriminator produced a usable union rather than a bare object; the deep dive is handling oneOf discriminators in generated SDKs. Finally, in CI, rely on the drift check from the integration workflow above — a clean git diff proves the committed SDK is exactly what the current spec generates.
Go one step further than compilation by exercising the client against a contract. Generate the SDK, then run a smoke test that constructs a request object and serializes it, confirming the field names and required-field handling match the spec. A type-level compile proves the shapes are internally consistent; a serialization round-trip proves they match what the API actually expects. Keep this test in the SDK package so it regenerates and reruns whenever the spec changes:
# Round-trip a sample model through the generated serializer.
cd sdks/typescript && npm test
# Asserts that Order { orderId, customerId } serializes to the wire shape the spec defines
When a generated client and a spec disagree, the round-trip test fails loudly at build time rather than silently in a consumer’s production traffic. Pair this with the spec validate step so you can always tell whether a failure originates in the spec or in the generated code.
Troubleshooting
Error: JAVA_HOME is not set / Unsupported class file major version. The generator found Java 8 or no JVM. Install Java 11+ and ensure it is first on PATH; in CI add the actions/setup-java@v4 step before generation. Verify with java -version.
Generation failed: discriminator ... is not defined on a oneOf schema. The schema lists oneOf variants but the discriminator.propertyName does not exist on each variant, or the mapping references a missing schema name. Add the discriminator property to every member schema and make the mapping keys match the values that actually appear in payloads.
Generated files reappear after you delete or edit them. You edited a file the generator owns; it gets recreated on the next run. Add the file’s path to .openapi-generator-ignore so the generator skips it, then make your edits — they will now persist across regenerations.
additionalProperties flag has no effect. Either the property name is wrong for that generator or it was passed with a space instead of =. Run config-help -g <generator> to confirm the exact property name, and pass it as --additional-properties=key=value (note the = and no spaces between comma-separated pairs).
Could not resolve reference while reading the spec. The spec uses a $ref to a file or URL the generator cannot reach from its working directory, which is common when paths are relative to a different root. Bundle the spec into one self-contained file first — npx @redocly/cli@1 bundle openapi.yaml -o bundled.yaml — and generate from the bundled output so every reference is already inlined.
Output directory keeps the previous run’s deleted operations. Removing an endpoint from the spec does not delete the corresponding generated file, because the generator only writes files it would produce, it does not clean stale ones. Delete the output directory before generating in CI, or use a clean checkout, so a removed operation does not linger as dead code in the published SDK.
FAQ
How do I pin the generator version so CI output is reproducible?
Run npx @openapitools/openapi-generator-cli version-manager set 7.7.0 and commit the resulting openapitools.json file. The CLI then downloads that exact generator JAR on every machine, so local and CI output match byte for byte and a transitive update never changes your SDK silently.
Why does my generated client overwrite my custom code on every run?
OpenAPI Generator regenerates every file it owns unless you list the file in .openapi-generator-ignore. Add the paths you maintain by hand to that ignore file and they survive regeneration, while everything else stays driven by the spec.
Can OpenAPI Generator produce SDKs for OpenAPI 3.1 specs?
Yes. The 3.1 parser has shipped since v7.0, so type arrays like [string, "null"] and the webhooks object are read correctly. Some individual generators lag on newer JSON Schema keywords, so validate polymorphic schemas against a fixture early rather than assuming full coverage.
How do I change the package name without editing the generated files?
Pass the language-specific property through --additional-properties, for example npmName for TypeScript or packageName for Python. Setting it on the command line or in openapitools.json keeps the name out of the generated source, so regeneration never reverts it.
Related
- SDK Generation & Changelog Automation — the parent pipeline this generator plugs into
- Customizing OpenAPI Generator templates — override Mustache templates for idiomatic output
- Generating a TypeScript Axios client from OpenAPI — the full TypeScript walkthrough
- Handling oneOf discriminators in generated SDKs — make polymorphic schemas generate clean unions
- Fern and Speakeasy — managed alternatives with built-in publishing