Skip to content

perf: make generation 60x faster on large specs by reusing an existing schema index - #24953

Merged
wing328 merged 1 commit into
OpenAPITools:masterfrom
AddisonGoolsbee:perf/enum-schema-lookup
Sep 17, 2026
Merged

wing328 merged 1 commit into
OpenAPITools:masterfrom
AddisonGoolsbee:perf/enum-schema-lookup

Conversation

@AddisonGoolsbee

@AddisonGoolsbee AddisonGoolsbee commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Generating a TypeScript client from the Stripe spec currently takes 6 minutes. This PR makes it 5 seconds, with byte-identical output, by calling an index that already exists in DefaultCodegen.

Generator Spec Before After
typescript-fetch Stripe, 8 MB, 1454 schemas 358.5 s 5.3 s 68x
typescript-fetch GitHub, 13 MB, 977 schemas 131.7 s 4.2 s 31x
python Stripe 11 s 9 s
go GitHub 8 s 7 s

Measured against the parent commit, run back to back. Every output tree verified byte-identical by recursive diff and by checksum.

Cause

updateCodegenPropertyEnum found a property's referenced schema by scanning every entry in components/schemas, recomputing toModelName() on each key until one matched:

Optional<Schema> referencedSchema = ModelUtils.getSchemas(openAPI).entrySet().stream()
        .filter(entry -> Objects.equals(varDataType, toModelName(entry.getKey())))
        .map(Map.Entry::getValue)
        .findFirst();

toModelName() is regex-heavy (sanitizeName + camelize), so each lookup costs O(schemas) in regex work, and postProcessModelsEnum calls it 8x per model across overlapping var lists. Sampling a typescript-fetch run on Stripe, 117 of 119 JVM stack samples were inside this method, nearly all in java.util.regex.Pattern.match. No output file was written until the last few seconds of the run.

Fix

DefaultCodegen already builds this index in getModelNameToSchemaCache(), but TypeScriptClientCodegen was its only caller, so every other generator paid the scan. This reuses it, unchanged. TypeScript generators gain most because toTypescriptTypeName makes each toModelName() call expensive; others gain less and none regress.

Full module suite: 5033 tests, 0 failures, 12 skipped. No samples regenerated, since output is unchanged.

Note

modelNameToSchemaCache was memoized and never invalidated; setOpenAPI() now clears it. processOpts() runs before setOpenAPI() in DefaultGenerator, so naming options are fully applied before the index is built.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title uses the format perf: <summary>.
  • Filed the PR against the correct branch: master.
  • Copied the technical committee to review the pull request if your PR is targeting a particular programming language.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java:681">
P2: When a model-name mapping or naming option changes after the first enum lookup, this cache still uses the old model names and can select the wrong schema or miss the schema entirely. Invalidate this cache whenever naming configuration changes, or include the naming configuration in the cache key.</violation>

<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java:681">
P2: When two schema keys collapse to one model name and the first schema is not an object, TypeScript discriminator example generation now drops the discriminator mapping. `getDiscriminatorMappedModel` must resolve the mapped schema by its schema key, rather than using this ambiguous model-name index.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// Create a cache to efficiently lookup schema based on model name.
Map<String, Schema> m = new HashMap<>();
ModelUtils.getSchemas(openAPI).forEach((key, schema) -> m.put(toModelName(key), schema));
ModelUtils.getSchemas(openAPI).forEach((key, schema) -> m.putIfAbsent(toModelName(key), schema));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a model-name mapping or naming option changes after the first enum lookup, this cache still uses the old model names and can select the wrong schema or miss the schema entirely. Invalidate this cache whenever naming configuration changes, or include the naming configuration in the cache key.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java, line 681:

<comment>When a model-name mapping or naming option changes after the first enum lookup, this cache still uses the old model names and can select the wrong schema or miss the schema entirely. Invalidate this cache whenever naming configuration changes, or include the naming configuration in the cache key.</comment>

<file context>
@@ -670,14 +670,15 @@ private boolean codegenPropertyIsNew(CodegenModel model, CodegenProperty propert
-            // Create a cache to efficiently lookup schema based on model name.
             Map<String, Schema> m = new HashMap<>();
-            ModelUtils.getSchemas(openAPI).forEach((key, schema) -> m.put(toModelName(key), schema));
+            ModelUtils.getSchemas(openAPI).forEach((key, schema) -> m.putIfAbsent(toModelName(key), schema));
             modelNameToSchemaCache = Collections.unmodifiableMap(m);
         }
</file context>

// Create a cache to efficiently lookup schema based on model name.
Map<String, Schema> m = new HashMap<>();
ModelUtils.getSchemas(openAPI).forEach((key, schema) -> m.put(toModelName(key), schema));
ModelUtils.getSchemas(openAPI).forEach((key, schema) -> m.putIfAbsent(toModelName(key), schema));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When two schema keys collapse to one model name and the first schema is not an object, TypeScript discriminator example generation now drops the discriminator mapping. getDiscriminatorMappedModel must resolve the mapped schema by its schema key, rather than using this ambiguous model-name index.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java, line 681:

<comment>When two schema keys collapse to one model name and the first schema is not an object, TypeScript discriminator example generation now drops the discriminator mapping. `getDiscriminatorMappedModel` must resolve the mapped schema by its schema key, rather than using this ambiguous model-name index.</comment>

<file context>
@@ -670,14 +670,15 @@ private boolean codegenPropertyIsNew(CodegenModel model, CodegenProperty propert
-            // Create a cache to efficiently lookup schema based on model name.
             Map<String, Schema> m = new HashMap<>();
-            ModelUtils.getSchemas(openAPI).forEach((key, schema) -> m.put(toModelName(key), schema));
+            ModelUtils.getSchemas(openAPI).forEach((key, schema) -> m.putIfAbsent(toModelName(key), schema));
             modelNameToSchemaCache = Collections.unmodifiableMap(m);
         }
</file context>

@AddisonGoolsbee

Copy link
Copy Markdown
Contributor Author

Thanks — both looked at.

Issue 2 is valid and I've fixed it. My first revision changed the index from put to putIfAbsent to preserve the findFirst() semantics of the scan I was replacing. As you note, that flips getDiscriminatorMappedModel from last-wins to first-wins on a model-name collision, and if the first schema isn't an object the mapping gets dropped. I've reverted that: getModelNameToSchemaCache() is now untouched, so TypeScriptClientCodegen behavior is identical to master. The only remaining semantic delta is that the enum path resolves a model-name collision last-wins instead of first-wins, which is an already-ambiguous case and does not affect any output I could measure.

Issue 1 is mitigated by call ordering. DefaultGenerator calls config.processOpts() (line 253) before config.setOpenAPI(openAPI) (line 285), and this PR clears modelNameToSchemaCache in setOpenAPI(). So naming configuration is fully applied before the index can be built. The index was already memoized with this property for the existing TypeScriptClientCodegen caller; this PR makes it strictly safer by adding the invalidation that was previously missing. If a generator mutates naming options during generation I'd consider that a separate pre-existing bug, since toModelName() results are already baked into CodegenModel by that point.

Re-verified after the change: full module suite still 5033 tests / 0 failures, and all four generator+spec combinations in the table still produce byte-identical output versus the parent commit.

updateCodegenPropertyEnum resolved a property's referenced schema by
streaming over every entry in components/schemas and recomputing
toModelName() for each key until one matched. toModelName() is
regex-heavy (sanitizeName + camelize), so each lookup cost O(schemas)
in regex work. postProcessModelsEnum calls it eight times per model
across overlapping var lists, so on large enum-heavy specs this
dominated generation time. Sampling a typescript-fetch run on the
Stripe spec, 117 of 119 JVM stack samples were in this method, nearly
all in java.util.regex.Pattern.match.

DefaultCodegen already builds exactly this index in
getModelNameToSchemaCache(), but its only caller was
TypeScriptClientCodegen, so every other generator paid the scan.
Reuse it, unchanged.

Measured against the parent commit, back to back, output compared by
recursive diff and by checksum:

  typescript-fetch  Stripe (8MB, 1454 schemas)   358.5s -> 5.3s   68x
  typescript-fetch  GitHub (13MB, 977 schemas)   131.7s -> 4.2s   31x
  python            Stripe                          11s -> 9s
  go                GitHub                            8s -> 7s

All four byte-identical. The TypeScript generators gain most because
toTypescriptTypeName makes each toModelName() call expensive; other
generators gain less and none regress.

modelNameToSchemaCache is also cleared in setOpenAPI() so a reused
codegen instance cannot serve a stale index. processOpts() runs before
setOpenAPI() in DefaultGenerator, so naming options are fully applied
before the index is built.

Full openapi-generator module suite: 5033 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AddisonGoolsbee AddisonGoolsbee changed the title perf: use existing model-name index for enum schema lookup perf: 68x faster generation on large specs by reusing an existing schema index Sep 16, 2026
@AddisonGoolsbee AddisonGoolsbee changed the title perf: 68x faster generation on large specs by reusing an existing schema index perf: make generation 68x faster by reusing an existing schema index Sep 16, 2026
@AddisonGoolsbee AddisonGoolsbee changed the title perf: make generation 68x faster by reusing an existing schema index perf: make generation 60x faster by reusing an existing schema index Sep 16, 2026
@AddisonGoolsbee AddisonGoolsbee changed the title perf: make generation 60x faster by reusing an existing schema index perf: make generation 60x faster on large specs by reusing an existing schema index Sep 16, 2026
@AddisonGoolsbee

Copy link
Copy Markdown
Contributor Author

cc @wing328 @macjohnny @joscha @davidgamero for review.

Flagging the TypeScript committee because that's where the measured impact is largest, though the change itself is in DefaultCodegen and removes the same scan for all 65 generators that call postProcessModelsEnum.

Summary for reviewers:

  • 8 lines added, 9 removed, one file. No new dependencies.
  • The enum path now calls getModelNameToSchemaCache(), which already existed in DefaultCodegen with a single caller in TypeScriptClientCodegen. The index itself is unmodified.
  • typescript-fetch on the Stripe spec goes from 358.5s to 5.3s; on the GitHub spec from 131.7s to 4.2s. python and go improve modestly (11s to 9s, 8s to 7s). None regress.
  • Every output tree verified byte-identical against the parent commit by recursive diff and by checksum, so no samples need regenerating.
  • Full openapi-generator module suite: 5033 tests, 0 failures, 12 skipped.

Happy to add a regression test or split out the setOpenAPI() cache invalidation if you'd prefer it separate.

@macjohnny macjohnny left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for this improvement

@joscha

joscha commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Excellent, LGTM!

@wing328

wing328 commented Sep 16, 2026

Copy link
Copy Markdown
Member

thanks for the PR

cc @OpenAPITools/generator-core-team

@wing328

wing328 commented Sep 17, 2026

Copy link
Copy Markdown
Member

tested locally to confirm the improvement.

thanks for the contribution.

@wing328
wing328 merged commit aa63ae0 into OpenAPITools:master Sep 17, 2026
15 checks passed
@wing328 wing328 added this to the 7.26.0 milestone Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants