From 2d958324ec3a3deda8cde6ea965ec6a6d0511e5e Mon Sep 17 00:00:00 2001 From: Shaun Hirst Date: Sat, 4 Jul 2026 21:31:30 +0100 Subject: [PATCH] fix(InlineModelResolver): do not deduplicate distinctly-named component schemas (#24177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deduplicateComponents() was added in #23856 to collapse numbered duplicates (e.g. FlowSegment_1 alongside FlowSegment) that the Swagger Parser / inline resolver produce when a shared multi-file schema fails to be reused. Its removal logic, however, matched any two titled, structurally-identical component schemas — including ones the user authored by hand with distinct, non-numbered names. Since 7.23.0 this silently dropped user-defined schemas (reported in #24177: 'upper1'/'upper2', identical title + structure, kept separate on purpose), removing previously-generated model classes. Two component keys can only both be non-numbered when the user deliberately named them that way — the parser always numbers its own collisions. Restrict removal to numbered names so the deduplication keeps collapsing generated artifacts while never deleting a distinctly-named user schema. Verified: full InlineModelResolverTest suite (57 tests) passes; the TAMS spec that motivated #23856 generates byte-for-byte identical output (modulo timestamps); the #24177 minimal spec now emits both Upper1 and Upper2. --- .../codegen/InlineModelResolver.java | 10 ++++++- .../codegen/InlineModelResolverTest.java | 30 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) 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")); + } }