How-to › Ship an SDK

How to find which template produced a generated file#

Read the FILES manifest for what OpenAPI Generator wrote, search its extracted template set for the file's text, and prove the match with a one-line marker.

Audience
API producer
Level
intermediate
Topic
Customize a generator without forking
Languages
TypeScript and Java
Verified

You want one line changed in the client OpenAPI Generator writes for you. The file is models/Meter.ts, the manifest lists thirteen outputs, and the generator ships forty templates for the target. There is no Meter.mustache, and models.mustache is two comment lines and four partial calls, so the afternoon goes on copying templates to see which one moves the output.

What you get

You will end up with a script that ranks the templates by the text they share with a generated file. A probe then proves the match by regenerating with one marked template. This is for you if you customize OpenAPI Generator output.

Short answer

Start with .openapi-generator/FILES in the output directory, which lists every file the last run wrote and nothing about how. Extract the generator’s own templates with openapi-generator author template -g <name> and search them for literal text from the file; the top hit is usually a partial, not the template whose name resembles the output. Confirm by adding one marker line to that template, regenerating with it alone in -t, and listing the files that carry the marker.

You will need

Java 11 or later for OpenAPI Generator, whose CLI is one jar on Maven Central, and Node 22 or later for the scripts. You also need an OpenAPI 3 document and the generated SDK checked in beside it. Verified 2026-10-21 against Java 21, Node 22.22.0, and openapi-generator-cli 7.26.0. The sample’s sdk/ directory is the typescript-fetch output for a three-operation document, which was generated once and committed. Every command regenerates into build/ from the committed document, and npm ci checks the jar in vendor/ against a recorded hash, fetching it when it is missing or does not match.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
OpenAPI Generator debug global propertiesYou want the normalized document the generator read, with every reference and default resolvedIn 7.26.0 only debugOpenAPI prints anything; debugModels, debugOperations, and debugSupportingFiles print a header and no bodyThe question is which file a template writes, which no dump records
Reading the shipped template setYou need to know how a file is produced, down to the lineAn extraction per generator release, forty files to search, and partials that keep the text away from the template whose name you knowYou only need to know whether the generator wrote a file at all
The .openapi-generator/FILES manifestYou need to know what the last run wrote, and whether a file is generated or hand-addedA list of output paths from the last run, with no template names on it and no history of earlier runsYou already know the file is generated and need the template

The three answer three different questions. The manifest says what was written, the debug properties say what the generator read, and the template set says how the text came to be. The last is the question a customization needs answered. The dumps are the cheapest to run and, in this release, answer least, because the model dumps print a header line and stop. The template set costs an extraction per release and a search, and it is the only one of the three that names a template.

Read the manifest first

Every run writes two files into .openapi-generator under the output directory: VERSION, the generator release, and FILES, the path of every file the run wrote.1 The script reads both and asks about one output.

node manifest.mjs models/Meter.ts
sdk/.openapi-generator/VERSION  7.26.0
sdk/.openapi-generator/FILES    13 files: 3 supporting, 2 api, 4 docs, 4 model
  supporting .openapi-generator-ignore
  api        apis/MetersApi.ts
  api        apis/index.ts
  docs       docs/Meter.md
  docs       docs/MeterUpdate.md
  docs       docs/MetersApi.md
  docs       docs/Problem.md
  supporting index.ts
  model      models/Meter.ts
  model      models/MeterUpdate.ts
  model      models/Problem.ts
  model      models/index.ts
  supporting runtime.ts

models/Meter.ts
  in FILES:   yes, the generator wrote it
  on disk:    yes
  template:   not recorded; the manifest names outputs, never templates

The manifest answers two questions. It says whether the last run wrote a path, so a file on disk that it does not list was added by hand, and it says which release wrote it. The template is recorded nowhere in the output, so the manifest is where a search starts and never where it ends.

Ask the generator for its model

The debugging documentation lists four global properties that print JSON. debugOpenAPI prints the document as the generator read it, debugModels the data handed to model templates, debugOperations the data for the API templates, and debugSupportingFiles the rest. The script runs all four against the sample document twice: at the default log level, then with debug logging on.

node dump.mjs
openapi-generator-cli 7.26.0, generating typescript-fetch with --dry-run
  --global-property debugModels=true
    header lines "Model info": 1   JSON lines after it: 0   with -Dlog.level=debug: 0
  --global-property debugOperations=true
    header lines "Operation info": 2   JSON lines after it: 0   with -Dlog.level=debug: 0
  --global-property debugSupportingFiles=true
    header lines "Supporting file info": 1   JSON lines after it: 0   with -Dlog.level=debug: 0
  --global-property debugOpenAPI=true
    JSON lines: 178   top-level keys: openapi, info, servers, paths, components   schemas: Meter, MeterUpdate, Problem

Three of the four print a header and stop, whether debug logging is on or off. The cause is a logging chain rather than a broken flag. The generator writes the header through its own logger and hands the body to swagger-core, whose logger the CLI holds at warn.2 debugOpenAPI prints with System.out and works, so the one dump you can get is the normalized document, 178 lines for three operations with every reference and default resolved. It tells you what the generator read. It names no template, because at that point none has run.

Search the template set for the file’s text

author template writes the generator’s embedded templates for one target into a directory, forty files for typescript-fetch. The finder cuts every mustache tag out of every template line, keeps the literal fragments of twelve characters or more, and counts how many of each template’s fragments the generated file contains.

export function fragments(templateText) {
  const out = new Set()
  for (const line of templateText.split('\n')) {
    for (const piece of line.split(/\{\{[^}]*\}\}+/)) {
      const text = piece.trim()
      if (text.length >= MIN) out.add(text)
    }
  }
  return [...out]
}

export function rank(generatedText) {
  return templates()
    .map((name) => {
      const all = fragments(read(join(TEMPLATES, name)))
      const hits = all.filter((f) => generatedText.includes(f))
      return { name, hits: hits.length, of: all.length }
    })
    .filter((r) => r.hits > 0)
    .sort((a, b) => b.hits - a.hits || a.name.localeCompare(b.name))
}
node find-template.mjs models/Meter.ts
models/Meter.ts: 40 templates in the typescript-fetch set, ranked by fragments the file contains
  modelGeneric.mustache             26 of  64 fragments   included by models.mustache
  modelOneOf.mustache               11 of  70 fragments   included by models.mustache
  modelEnum.mustache                 7 of  18 fragments   included by models.mustache
  apis.mustache                      5 of 147 fragments
  runtime.mustache                   5 of 329 fragments

The top hit is modelGeneric.mustache, with 26 of its 64 fragments in the file, and it is a partial.3 models.mustache, the template the generator registers for models, is two comment lines and four partial calls: a license header, then a choice between modelEnum, modelOneOf, and modelGeneric. The second and third hits are the other two partials it chooses between, for a oneOf schema and for an enum, which share lines with the generic one. The same search on apis/MetersApi.ts puts apis.mustache first with 51 fragments, and on docs/Meter.md puts model_doc.mustache first.

Confirm with one marker line

A ranking is a strong hint and a regeneration is proof. The probe copies one template into an empty directory, appends a comment naming it, and regenerates with that directory as -t. Every other template falls back to the embedded set, in the lookup order the templating documentation gives: your directory first, the embedded files after.

export function probe(templateName) {
  templates()
  const dir = `build/probe/${templateName.replace(/\.mustache$/, '')}`
  rmSync(dir, { recursive: true, force: true })
  mkdirSync(join(dir, 'templates'), { recursive: true })
  const marker = `// probe: ${templateName}`
  writeFileSync(join(dir, 'templates', templateName), read(join(TEMPLATES, templateName)) + `\n${marker}\n`)
  generate(join(dir, 'out'), ['-t', join(dir, 'templates')])
  return tree(join(dir, 'out')).filter((f) => read(join(dir, 'out', f)).includes(marker))
}
node confirm.mjs modelGeneric.mustache
modelGeneric.mustache with one marker line, regenerated with only that file in -t
  files carrying the marker: 3
    models/Meter.ts
    models/MeterUpdate.ts
    models/Problem.ts

The marker reaches the three model files and no other output, which is the answer the manifest and the dumps cannot give: this template, these outputs, in this release. The probe directory holds one file, so the regeneration is otherwise the stock one, and the diff against the committed client is three comment lines.

Check it worked

node --test find.test.mjs
1..9
# tests 9
# suites 0
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0

Nine tests. VERSION equals what the jar reports, and FILES equals the tree on disk with the metadata left out. A regeneration from the committed document reproduces every committed file byte for byte, which is the check that makes the committed client trustworthy at all. The ranking puts the right template first for a model, an API, the runtime, and a documentation page. modelGeneric is included by models.mustache alone, and the two probes land where the outputs say. The last two keep the inputs pinned: a jar on disk that fails the recorded hash is fetched again, and a template set that another jar extracted is extracted again.

When it goes wrong

node pitfall.mjs
models/index.ts: the template the name suggests, then the one the text is in
  index.mustache             marker lands in: index.ts
  models.index.mustache      marker lands in: models/index.ts

models/Meter.ts: no template is called Meter, and the one called models holds two literal lines
  templates with "meter" in the name: none
  top template by text:               modelGeneric.mustache, 26 fragments
  literal lines in models.mustache:   2: /* tslint:disable */ | /* eslint-disable */

The name misleads. models/index.ts looks like the work of index.mustache, and a marker there lands in the top-level index.ts only; models.index.mustache is the template, with a dot where the directory separator is. For models/Meter.ts there is no template with the model’s name, because one template writes every model, and the template that carries the directory’s name holds two literal lines.

The dump is empty. --global-property debugModels=true prints the header and nothing after it, and -Dlog.level=debug changes nothing, because it raises the generator’s own logger and leaves the one the body goes through at warn. Use debugOpenAPI for the document and the template set for the model.

The templates are from the wrong release. A set extracted from a newer jar than the one in VERSION matches lines that have since moved, and a probe against it regenerates a different client. Extract from the jar the manifest names, which is what the first test pins. generator.mjs stamps the set with the SHA-256 of the jar that extracted it and extracts again when the jar changes, which the last test pins.

When not to do this

Do not copy the whole template set to change one line. The probe here works because its directory holds one file and every other template falls back to the embedded set, and a customization built the same way stays one reviewable file. A full copy freezes forty files against every later release and hides the one you changed.

Do not edit the generated file instead of finding its template. The edit survives until the next run, and the run that removes it is the one somebody else starts.

Do not read the manifest as a list of what is safe to change. It says what the generator wrote, not what it owns. A file it lists is rewritten on the next run whether or not you edited it, and the ignore file is the only thing that changes that.

A template set from another release is the wrong one to search, and an extracted set does not belong in the repository. Extract from the jar VERSION names, into a build directory, each time you need it.

Last verified

Verified 2026-10-21 against Java 21, Node 22.22.0, and openapi-generator-cli 7.26.0, fetched from Maven Central by npm ci. Every output block is what the command preceding it printed. The logging chain behind the empty dumps was read from the generator and swagger-core sources at the tags the notes name, not stepped through in a debugger.

Footnotes

  1. The manifest’s path separator is settled in a source comment. The generator writes Linux-style paths to FILES on purpose, the comment explaining that File.separator would produce paths that fail to match on Windows. A second comment beside it guards against a null path seen once on CI, to be fixed, it says, in the 5.0 template management refactor. The code shipping in 7.26.0 still carries the note. ↩︎ Back to text

  2. The chain has three links in two repositories. In 7.26.0 the generator logs the header with its own logger at info and calls Json.prettyPrint for the body. That method, in swagger-core 2.2.52, serializes the object and hands the string to a helper that logs it at debug on the Json class’s own logger. The CLI’s logback.xml sets io.swagger to warn, org.openapitools to the log.level property with info as the default, and the root to error. So -Dlog.level=debug opens the generator’s logger and leaves the one carrying the body closed, which is why the header prints with nothing under it. ↩︎ Back to text

  3. The mustache(5) manual says partials are rendered at runtime rather than compile time, so recursive partials are possible, and advises avoiding infinite loops. It then suggests thinking of partials as includes, or template expansion, and adds in the same sentence that this is not literally true. A template whose text lives in four other files is the ordinary case the manual is describing, with the include picture offered and withdrawn in one breath. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.