diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java
index 4add57db345a..c1b3456b7a6d 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java
@@ -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;
@@ -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)) {
return uniqueName;
}
uniqueName = name + "_" + ++count;
}
}
+ /**
+ * Whether the candidate schema name collides with an existing one.
+ *
+ *
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.
+ *
+ *
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 properties, String path) {
if (properties == null) {
return;
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java
index caf66155197d..b9d0c5188303 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java
@@ -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"));
+ }
}