Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import io.swagger.v3.oas.models.responses.ApiResponses;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.utils.ModelUtils;

import static org.openapitools.codegen.utils.StringUtils.camelize;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -924,13 +926,53 @@ private String uniqueName(final String name) {
String uniqueName = name;
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>

return uniqueName;
}
uniqueName = name + "_" + ++count;
}
}

/**
* Whether the candidate schema name collides with an existing one.
*
* <p>Names are compared as the default model mangler would emit them, i.e.
* {@code camelize(sanitizeName(name))}: the inline body schema {@code importMembers_request} and
* a spec-defined {@code ImportMembersRequest} both become the model {@code ImportMembersRequest}.
* Comparing the raw keys only, the two look distinct, the inline schema is registered under its
* own key, and later one model silently overwrites the other - the properties of the overwritten
* model disappear from the generated code. Treating them as a collision yields
* {@code importMembers_request_1} instead, so both models survive.
*
* <p>Mirroring the mangler rather than lowercasing keeps names that really do stay distinct
* apart: {@code Importmembersrequest} mangles to itself, not to {@code ImportMembersRequest}.
*
* @param candidate the candidate name
* @return true if the name is already taken
*/
private boolean isNameTaken(final String candidate) {
if (openAPI.getComponents().getSchemas().containsKey(candidate) || uniqueNames.contains(candidate)) {
return true;
}

String normalized = normalizeNameForCollision(candidate);
for (String existing : openAPI.getComponents().getSchemas().keySet()) {
if (normalized.equals(normalizeNameForCollision(existing))) {
return true;
}
}
for (String existing : uniqueNames) {
if (normalized.equals(normalizeNameForCollision(existing))) {
return true;
}
}
return false;
}

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

private void flattenProperties(OpenAPI openAPI, Map<String, Schema> properties, String path) {
if (properties == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1666,4 +1666,96 @@ public void deduplicateComponentsRewritesRefsOutsideComponentsSchemas() {
assertRewritten("encoding headers", response.getContent().get("application/json")
.getEncoding().get("ok").getHeaders().get("X-Encoding").getSchema());
}

@Test
public void inlineSchemaDoesNotOverwriteSpecDefinedSchemaWithMangledSameName() {
// The inline request body schema is registered as "importMembers_request" while the spec
// also defines "ImportMembersRequest". Both mangle to the same model name, so without
// normalization-aware collision detection one silently overwrites the other and the
// properties of the overwritten model disappear from the generated code.
OpenAPI openAPI = new OpenAPI();
openAPI.setComponents(new Components());
openAPI.setPaths(new Paths());

openAPI.getComponents().addSchemas("ImportMembersRequest", new ObjectSchema()
.addProperty("declaredField", new StringSchema()));

Schema inlineBody = new ObjectSchema().addProperty("inlineOnly", new StringSchema());
openAPI.getPaths().addPathItem("/members/import", new PathItem().post(new Operation()
.operationId("importMembers")
.requestBody(new RequestBody().content(new Content()
.addMediaType("application/json", new MediaType().schema(inlineBody))))));

new InlineModelResolver().flatten(openAPI);

// the spec-defined schema must survive untouched
Schema declared = openAPI.getComponents().getSchemas().get("ImportMembersRequest");
assertNotNull(declared);
assertNotNull(declared.getProperties().get("declaredField"));
assertNull(declared.getProperties().get("inlineOnly"));

// the inline schema must be registered under a name that does not mangle onto the
// spec-defined one
assertNull(openAPI.getComponents().getSchemas().get("importMembers_request"));
Schema inlined = openAPI.getComponents().getSchemas().get("importMembers_request_1");
assertNotNull(inlined);
assertNotNull(inlined.getProperties().get("inlineOnly"));
}

@Test
public void inlineSchemaDoesNotOverwriteSpecDefinedSchemaNamedWithOtherSeparators() {
// sanitizeName turns '-', '.', ' ' and friends into '_' before the name is camelized, so
// `Import-Members-Request` mangles onto the same model as the inline `importMembers_request`.
// Normalizing on '_' alone would miss this.
OpenAPI openAPI = new OpenAPI();
openAPI.setComponents(new Components());
openAPI.setPaths(new Paths());

openAPI.getComponents().addSchemas("Import-Members-Request", new ObjectSchema()
.addProperty("declaredField", new StringSchema()));

Schema inlineBody = new ObjectSchema().addProperty("inlineOnly", new StringSchema());
openAPI.getPaths().addPathItem("/members/import", new PathItem().post(new Operation()
.operationId("importMembers")
.requestBody(new RequestBody().content(new Content()
.addMediaType("application/json", new MediaType().schema(inlineBody))))));

new InlineModelResolver().flatten(openAPI);

Schema declared = openAPI.getComponents().getSchemas().get("Import-Members-Request");
assertNotNull(declared);
assertNotNull(declared.getProperties().get("declaredField"));
assertNull(declared.getProperties().get("inlineOnly"));

assertNull(openAPI.getComponents().getSchemas().get("importMembers_request"));
Schema inlined = openAPI.getComponents().getSchemas().get("importMembers_request_1");
assertNotNull(inlined);
assertNotNull(inlined.getProperties().get("inlineOnly"));
}

@Test
public void inlineSchemaKeepsItsNameWhenTheManglerKeepsNamesDistinct() {
// `Importmembersrequest` mangles to itself, not to `ImportMembersRequest`, so it is NOT a
// collision -- the inline schema must keep its own name rather than being pushed to _1.
OpenAPI openAPI = new OpenAPI();
openAPI.setComponents(new Components());
openAPI.setPaths(new Paths());

openAPI.getComponents().addSchemas("Importmembersrequest", new ObjectSchema()
.addProperty("declaredField", new StringSchema()));

Schema inlineBody = new ObjectSchema().addProperty("inlineOnly", new StringSchema());
openAPI.getPaths().addPathItem("/members/import", new PathItem().post(new Operation()
.operationId("importMembers")
.requestBody(new RequestBody().content(new Content()
.addMediaType("application/json", new MediaType().schema(inlineBody))))));

new InlineModelResolver().flatten(openAPI);

assertNotNull(openAPI.getComponents().getSchemas().get("Importmembersrequest"));
Schema inlined = openAPI.getComponents().getSchemas().get("importMembers_request");
assertNotNull("no real collision, the inline schema should keep its own name", inlined);
assertNotNull(inlined.getProperties().get("inlineOnly"));
assertNull(openAPI.getComponents().getSchemas().get("importMembers_request_1"));
}
}
Loading