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")); + } }