Skip to content

fix: stop inline schemas from silently overwriting spec-defined models - #24549

Open
donald wants to merge 3 commits into
OpenAPITools:masterfrom
donald:fix/inline-model-name-collision
Open

fix: stop inline schemas from silently overwriting spec-defined models#24549
donald wants to merge 3 commits into
OpenAPITools:masterfrom
donald:fix/inline-model-name-collision

Conversation

@donald

@donald donald commented Aug 1, 2026

Copy link
Copy Markdown

Fixes #24548

What

An inline request-body schema could silently replace a schema declared in components.schemas,
deleting its properties from the generated code. No error, no warning — the generated client just has
the wrong contract for an endpoint that never used an inline schema.

paths:
  /members/import:
    post:
      operationId: importMembers        # inline body -> "importMembers_request"
      requestBody: { content: { application/json: { schema: { type: object, properties: { inlineOnly: { type: string } } } } } }
  /members/echo:
    post:
      requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/ImportMembersRequest" } } } }
components:
  schemas:
    ImportMembersRequest:               # mangles to the same model name
      properties: { declaredFieldA: { type: string }, declaredFieldB: { type: integer } }
// before -- only ONE model file is emitted
export interface ImportMembersRequest {
    inlineOnly?: string;                // the inline body schema won
}
// declaredFieldA / declaredFieldB are gone, and /members/echo now serialises the wrong shape

// after -- both models survive
export interface ImportMembersRequest  { declaredFieldA?: string; declaredFieldB?: number; }
export interface ImportMembersRequest1 { inlineOnly?: string; }

Why

InlineModelResolver.uniqueName()
compares candidate names by raw string:

if (!openAPI.getComponents().getSchemas().containsKey(uniqueName) && !uniqueNames.contains(uniqueName)) {
    return uniqueName;
}

Generators mangle schema names before emitting types, so importMembers_request and
ImportMembersRequest both become the model ImportMembersRequest. The raw keys differ, no collision is
detected, the inline schema is registered under its own key, and later one model overwrites the other.

How

Make the collision check normalization-aware: compare names ignoring separators and case — i.e. the way
they will look after mangling. The existing name + "_" + count fallback then does the rest, so the
inline schema lands on importMembers_request_1 and both models survive.

This only ever makes uniqueName() pick a different (numbered) name where it previously returned one
that would collide after mangling; it cannot merge or rename anything that was already distinct.

Testing

  • InlineModelResolverTest.inlineSchemaDoesNotOverwriteSpecDefinedSchemaWithMangledSameName
    asserts the spec-defined schema keeps its own properties and the inline schema is registered under a
    non-colliding key. Verified that this test fails without the fix.
  • Full InlineModelResolverTest (60 tests) passes.
  • Regenerated all 787 sample configs (./bin/generate-samples.sh ./bin/configs/*.yaml) and
    ./bin/utils/export_docs_generators.shno diff. No existing sample hits the collision, so the
    change is inert on the entire sample corpus.

Note on the full test run: mvn test on modules/openapi-generator reports one pre-existing failure,
OpenApiSchemaValidationsTest.testNullTypeInOas31_noWarning. It fails identically on unmodified
master (4aed9c1c635) — verified by reverting the patch and re-running — and is unrelated to this change.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    Commit all changed files.
    (Both scripts were run; neither produced any change, so there is nothing to commit beyond the fix and its test.)
  • File the PR against the correct branch: master
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

This is in the language-agnostic core (InlineModelResolver), so it is not owned by one language committee.
CC @wing328

🤖 Generated with Claude Code


Summary by cubic

Prevents inline request-body schemas from overwriting spec-defined models after name mangling. Collision checks now use the same normalization as the model mangler so real collisions are numbered and distinct names stay distinct.

  • Bug Fixes
    • Update InlineModelResolver.uniqueName() to compare names via camelize(sanitizeName(name)); fallback to name_1 when needed.
    • Add tests: inlineSchemaDoesNotOverwriteSpecDefinedSchemaWithMangledSameName, inlineSchemaDoesNotOverwriteSpecDefinedSchemaNamedWithOtherSeparators, inlineSchemaKeepsItsNameWhenTheManglerKeepsNamesDistinct.
    • Regenerate all samples; no changes.

Written for commit dfff3c4. Summary will update on new commits.

Review in cubic

An inline request-body schema could silently replace a schema declared in
components.schemas, deleting its properties from the generated code. No
error, no warning -- the generated client just has the wrong contract for
an endpoint that never used an inline schema:

    paths:
      /members/import:
        post:
          operationId: importMembers      # inline body -> importMembers_request
    components:
      schemas:
        ImportMembersRequest:             # mangles to the same model name
          properties: { declaredFieldA: ..., declaredFieldB: ... }

Only one model was emitted, carrying the inline schema's properties;
declaredFieldA/declaredFieldB disappeared, and the endpoint that $refs
ImportMembersRequest silently serialised the wrong shape.

InlineModelResolver.uniqueName() compared candidate names by raw string
only. Generators mangle schema names before emitting types, so
"importMembers_request" and "ImportMembersRequest" both become the model
"ImportMembersRequest" while the raw keys look distinct -- no collision is
detected and one model overwrites the other.

Make the collision check normalization-aware: compare names ignoring
separators and case, i.e. the way they will look after mangling. The
existing name + "_" + count fallback then keeps both models, emitting
ImportMembersRequest and ImportMembersRequest1.

Regenerating all 787 sample configs produces no diff.

@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.

1 issue found across 2 files

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/InlineModelResolver.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java:967">
P2: The normalization in `normalizeNameForCollision()` only strips underscores (`_`) before lowercasing. Spec-defined schema names stored in `openAPI.getComponents().getSchemas()` can contain hyphens (`-`), dots (`.`), or other non-alphanumeric characters that `sanitizeName` would eventually convert to `_`. For example, a spec-defined schema named `Import-Members-Request` normalizes to `import-members-request` (hyphen not removed), while an inline schema `importMembers_request` normalizes to `importmembersrequest` — the collision is missed, and the silent overwrite issue could still occur for schemas using other separator characters. Since the Javadoc says the method "ignores separators and case," stripping only `_` is narrower than described.</violation>
</file>

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

Re-trigger cubic

Comment on lines +967 to +968
private String normalizeNameForCollision(final String name) {
return name.replace("_", "").toLowerCase(Locale.ROOT);

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: The normalization in normalizeNameForCollision() only strips underscores (_) before lowercasing. Spec-defined schema names stored in openAPI.getComponents().getSchemas() can contain hyphens (-), dots (.), or other non-alphanumeric characters that sanitizeName would eventually convert to _. For example, a spec-defined schema named Import-Members-Request normalizes to import-members-request (hyphen not removed), while an inline schema importMembers_request normalizes to importmembersrequest — the collision is missed, and the silent overwrite issue could still occur for schemas using other separator characters. Since the Javadoc says the method "ignores separators and case," stripping only _ is narrower than described.

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/InlineModelResolver.java, line 967:

<comment>The normalization in `normalizeNameForCollision()` only strips underscores (`_`) before lowercasing. Spec-defined schema names stored in `openAPI.getComponents().getSchemas()` can contain hyphens (`-`), dots (`.`), or other non-alphanumeric characters that `sanitizeName` would eventually convert to `_`. For example, a spec-defined schema named `Import-Members-Request` normalizes to `import-members-request` (hyphen not removed), while an inline schema `importMembers_request` normalizes to `importmembersrequest` — the collision is missed, and the silent overwrite issue could still occur for schemas using other separator characters. Since the Javadoc says the method "ignores separators and case," stripping only `_` is narrower than described.</comment>

<file context>
@@ -924,13 +924,50 @@ private String uniqueName(final String name) {
+        return false;
+    }
+
+    private String normalizeNameForCollision(final String name) {
+        return name.replace("_", "").toLowerCase(Locale.ROOT);
+    }
</file context>
Suggested change
private String normalizeNameForCollision(final String name) {
return name.replace("_", "").toLowerCase(Locale.ROOT);
private String normalizeNameForCollision(final String name) {
return name.replaceAll("[^A-Za-z0-9]", "").toLowerCase(Locale.ROOT);
}

…ting collisions

Addresses review feedback on normalizeNameForCollision.

sanitizeName turns '-', '.', ' ' and friends into '_' before the name is
camelized, so a spec-defined `Import-Members-Request` mangles onto the same
model as the inline `importMembers_request`. Stripping only '_' missed those,
and was also narrower than the javadoc's "ignores separators and case".

Strip every non-alphanumeric character instead.

New test inlineSchemaDoesNotOverwriteSpecDefinedSchemaNamedWithOtherSeparators
covers a hyphenated spec-defined name; it fails on the previous version.

Regenerating all 787 sample configs still produces no diff.
@donald

donald commented Aug 1, 2026

Copy link
Copy Markdown
Author

Good catch — fixed in 17162a9.

You are right, and the javadoc claim ("ignores separators and case") was wider than the implementation. sanitizeName turns -, ., spaces and friends into _ before the name is camelized, so Import-Members-Request mangles onto the same model as importMembers_request and stripping only _ missed it — the silent-overwrite problem was still reachable for those names.

Now every non-alphanumeric character is stripped.

Added InlineModelResolverTest.inlineSchemaDoesNotOverwriteSpecDefinedSchemaNamedWithOtherSeparators covering a hyphenated spec-defined name; it fails on the previous version.

Full test suite and all 787 sample configs re-run — still no diff.

@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 2 files

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/InlineModelResolver.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java:930">
P1: An inline schema name mapping can still overwrite an existing component schema, so the new guard does not cover the final name written to `components.schemas`. Collision checking should include the mapped target and choose a non-colliding mapped name before insertion.</violation>

<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java:974">
P2: Valid schema names that differ only by letter case are unnecessarily renamed even though the default model mangler keeps them distinct. Collision normalization should mirror the target mangler instead of lowercasing every letter.</violation>
</file>

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

Re-trigger cubic

int count = 0;
while (true) {
if (!openAPI.getComponents().getSchemas().containsKey(uniqueName) && !uniqueNames.contains(uniqueName)) {
if (!isNameTaken(uniqueName)) {

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.

P1: An inline schema name mapping can still overwrite an existing component schema, so the new guard does not cover the final name written to components.schemas. Collision checking should include the mapped target and choose a non-colliding mapped name before insertion.

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/InlineModelResolver.java, line 930:

<comment>An inline schema name mapping can still overwrite an existing component schema, so the new guard does not cover the final name written to `components.schemas`. Collision checking should include the mapped target and choose a non-colliding mapped name before insertion.</comment>

<file context>
@@ -924,13 +927,53 @@ private String uniqueName(final String name) {
         int count = 0;
         while (true) {
-            if (!openAPI.getComponents().getSchemas().containsKey(uniqueName) && !uniqueNames.contains(uniqueName)) {
+            if (!isNameTaken(uniqueName)) {
                 return uniqueName;
             }
</file context>

// Every non-alphanumeric character is a separator here, not just '_': sanitizeName turns
// '-', '.', ' ' and friends into '_' before the name is camelized, so `Import-Members-Request`
// and `importMembers_request` end up as the same model too.
return NON_ALPHANUMERIC.matcher(name).replaceAll("").toLowerCase(Locale.ROOT);

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: Valid schema names that differ only by letter case are unnecessarily renamed even though the default model mangler keeps them distinct. Collision normalization should mirror the target mangler instead of lowercasing every letter.

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/InlineModelResolver.java, line 974:

<comment>Valid schema names that differ only by letter case are unnecessarily renamed even though the default model mangler keeps them distinct. Collision normalization should mirror the target mangler instead of lowercasing every letter.</comment>

<file context>
@@ -924,13 +927,53 @@ private String uniqueName(final String name) {
+        // Every non-alphanumeric character is a separator here, not just '_': sanitizeName turns
+        // '-', '.', ' ' and friends into '_' before the name is camelized, so `Import-Members-Request`
+        // and `importMembers_request` end up as the same model too.
+        return NON_ALPHANUMERIC.matcher(name).replaceAll("").toLowerCase(Locale.ROOT);
+    }
+
</file context>

…sing

Addresses review feedback that lowercasing over-matches.

Names that differ only by letter case can stay distinct after mangling, so
folding case flagged collisions that do not exist:

    importMembers_request   -> ImportMembersRequest
    Import-Members-Request  -> ImportMembersRequest   (real collision)
    ImportMembersRequest    -> ImportMembersRequest   (real collision)
    Importmembersrequest    -> Importmembersrequest   (distinct!)
    IMPORTMEMBERSREQUEST    -> IMPORTMEMBERSREQUEST   (distinct!)

The last two would have been pushed to _1 for no reason.

Rather than hand-rolling a closer approximation, just run the names through
the mangler itself -- camelize(sanitizeName(name)). That catches every real
collision, cannot over-match by construction, and makes the javadoc's claim
verifiable instead of aspirational.

New test inlineSchemaKeepsItsNameWhenTheManglerKeepsNamesDistinct covers the
false-positive direction; it fails on the previous version with "no real
collision, the inline schema should keep its own name".

Regenerating all 787 sample configs still produces no diff.
@donald

donald commented Aug 1, 2026

Copy link
Copy Markdown
Author

Thanks — one adopted, one I would like to push back on.

P2 (case folding over-matches) — valid, fixed in dfff3c4

You are right. Checking against the real mangler:

importMembers_request   -> ImportMembersRequest
Import-Members-Request  -> ImportMembersRequest   (real collision)
ImportMembersRequest    -> ImportMembersRequest   (real collision)
Importmembersrequest    -> Importmembersrequest   (distinct!)
IMPORTMEMBERSREQUEST    -> IMPORTMEMBERSREQUEST   (distinct!)

The last two would have been pushed to _1 for no reason.

Rather than hand-rolling a closer approximation, I now run the names through the mangler itself:

private String normalizeNameForCollision(final String name) {
    return camelize(sanitizeName(name));
}

That catches every real collision, cannot over-match by construction, and makes the javadoc claim verifiable instead of aspirational. It also subsumes the separator fix from the previous round. Added inlineSchemaKeepsItsNameWhenTheManglerKeepsNamesDistinct covering the false-positive direction; it fails on the previous version.

P1 (inline schema name mapping) — I do not think this belongs in this PR

Two reasons:

It is pre-existing and unrelated to this change. addSchemas() applies inlineSchemaNameMapping after uniqueName() has run and then calls addSchemas(name, schema) directly. That path bypassed uniqueName() before this PR too — this PR only changes uniqueName()/isNameTaken(), so it introduces no regression here, it just does not happen to also fix that.

Silently renaming would defeat the option. inlineSchemaNameMapping exists so the user can pick the name explicitly. Quietly turning their --inline-schema-name-mappings foo=Bar into Bar_1 because it collides is very likely the wrong resolution; failing, or at least warning loudly, seems more appropriate. That is a design decision worth its own discussion rather than a drive-by change inside a bugfix.

Happy to open a separate issue for it if maintainers agree it should be addressed.

Full test suite and all 787 sample configs re-run — still no diff.

@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.

No issues found across 2 files

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Inline request-body schema silently overwrites a spec-defined schema with a mangled-equal name

1 participant