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 d5d9b348c44e..7294c67a2ab0 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 @@ -1223,7 +1223,15 @@ private void deduplicateComponents() { String sigKey = schema.getTitle() + "||" + structural; String canonical = canonicalBySig.get(sigKey); if (canonical != null) { - duplicateToCanonical.put(name, canonical); + // Only collapse numbered duplicates (e.g. FlowSegment_1) — those are artifacts + // the Swagger Parser / inline resolver produced when it failed to reuse an + // existing component. A schema the user authored with its own distinct, + // non-numbered name is part of the spec's source of truth and must be kept even + // when it is structurally identical to another named schema (see #24177: two + // schemas 'upper1'/'upper2' with the same title, intentionally distinct). + if (name.matches(".*_\\d+$")) { + duplicateToCanonical.put(name, canonical); + } } else { canonicalBySig.put(sigKey, name); } 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 81b30649f368..b567b28c9ee5 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 @@ -1487,4 +1487,34 @@ public void deduplicateComponentsRemovesNumberedDuplicateOfTitledSchemaAndRewrit assertEquals("$ref must be rewritten from Widget_1 to Widget", "#/components/schemas/Widget", responseSchema.get$ref()); } + + @Test + public void deduplicateComponentsKeepsDistinctlyNamedUserSchemas() { + // Regression test for #24177: two component schemas the user authored with distinct, + // non-numbered names (upper1 / upper2) that happen to be structurally identical AND share + // the same title must NOT be merged. deduplicateComponents() only collapses numbered + // duplicates generated by the parser/inline resolver (Foo_1 alongside Foo); user-authored + // named types are the source of truth and must both survive. + OpenAPI openapi = new OpenAPI(); + openapi.setComponents(new Components()); + + Schema upper1 = new ObjectSchema() + .title("foo title identical by purpose") + .description("foo") + .addProperty("foo", new StringSchema()); + Schema upper2 = new ObjectSchema() + .title("foo title identical by purpose") + .description("foo") + .addProperty("foo", new StringSchema()); + + openapi.getComponents().addSchemas("upper1", upper1); + openapi.getComponents().addSchemas("upper2", upper2); + + new InlineModelResolver().flatten(openapi); + + Map schemas = openapi.getComponents().getSchemas(); + assertNotNull("upper1 must survive", schemas.get("upper1")); + assertNotNull("upper2 must survive — distinctly-named user schemas must not be deduplicated", + schemas.get("upper2")); + } }