Skip to content

Commit e7bf897

Browse files
committed
fix(InlineModelResolver): do not merge untitled inline schemas differing only by description
The structural-signature dedup fallback added for multi-file OAS 3.1 specs (#23856) strips description/type/example at every level and was applied to all schemas in matchGenerated()/addGenerated(). For untitled inline schemas that are structurally identical once those fields are stripped but intentionally distinct (e.g. two response properties differing only by description), this collapsed them into a single generated type, changing one property's type and breaking compiling user code (regression since 7.23). Restrict the structural-signature fallback to titled schemas only, mirroring the existing titled-only guards in flatten() pre-population and deduplicateComponents() ("anonymous schemas may be intentionally distinct"). Exact-signature matching is unaffected, so genuine duplicates are still reused. Fixes #24004 Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com>
1 parent a179190 commit e7bf897

2 files changed

Lines changed: 74 additions & 4 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -862,9 +862,20 @@ private String matchGenerated(Schema model) {
862862
}
863863
// Structural match: compare with volatile fields stripped at every level.
864864
// See generatedStructuralSignature field for a full explanation of why this is needed.
865-
String structural = computeStructuralSignature(model);
866-
if (generatedStructuralSignature.containsKey(structural)) {
867-
return generatedStructuralSignature.get(structural);
865+
//
866+
// Only applied to *titled* schemas. A title denotes a named type that should be reused
867+
// wherever it appears, so parser-induced volatile differences (description, type,
868+
// example) must not split it into numbered duplicates. Anonymous/untitled inline
869+
// schemas, however, may be intentionally distinct even when structurally identical once
870+
// those volatile fields are stripped (e.g. two response properties that differ only by
871+
// description) — unifying them silently changes the generated type of one property and
872+
// breaks user code. This mirrors the titled-only guards in flatten() pre-population and
873+
// deduplicateComponents().
874+
if (model.getTitle() != null) {
875+
String structural = computeStructuralSignature(model);
876+
if (generatedStructuralSignature.containsKey(structural)) {
877+
return generatedStructuralSignature.get(structural);
878+
}
868879
}
869880
} catch (JsonProcessingException e) {
870881
e.printStackTrace();
@@ -876,7 +887,11 @@ private String matchGenerated(Schema model) {
876887
private void addGenerated(String name, Schema model) {
877888
try {
878889
generatedSignature.put(structureMapper.writeValueAsString(model), name);
879-
generatedStructuralSignature.putIfAbsent(computeStructuralSignature(model), name);
890+
// Only register the volatile-stripped structural signature for titled schemas; untitled
891+
// inline schemas must not participate in the structural-match fallback (see matchGenerated).
892+
if (model.getTitle() != null) {
893+
generatedStructuralSignature.putIfAbsent(computeStructuralSignature(model), name);
894+
}
880895
} catch (JsonProcessingException e) {
881896
e.printStackTrace();
882897
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,61 @@ public void resolveInlineModelDeduplicatesWhenParserMutatesPropertyDescriptions(
274274
assertNull("Duplicate Widget_1 must not exist — shape-fingerprint dedup must fire", schemas.get("Widget_1"));
275275
}
276276

277+
@Test
278+
public void resolveInlineModelKeepsUntitledSchemasDifferingOnlyByDescriptionDistinct() {
279+
// Regression test for #24004: two distinct *untitled* inline object schemas that differ
280+
// only in their descriptions (here the nested 'result' property: "ABC Result" vs
281+
// "DEF Result") must NOT be merged. The volatile-stripped structural-signature fallback in
282+
// matchGenerated() collapses them once description/type are removed; that fallback is only
283+
// intended to unify titled named types across parser volatility, so it must not fire for
284+
// untitled inline schemas — otherwise 'def' silently gets the type generated for 'abc' and
285+
// breaks user code that expects two separate types (regression introduced in 7.23).
286+
OpenAPI openapi = new OpenAPI();
287+
openapi.setComponents(new Components());
288+
openapi.setPaths(new Paths());
289+
290+
Schema abc = new ObjectSchema()
291+
.description("first container")
292+
.addProperty("result", new StringSchema().description("ABC Result"));
293+
Schema def = new ObjectSchema()
294+
.description("second container")
295+
.addProperty("result", new StringSchema().description("DEF Result"));
296+
297+
Schema response = new ObjectSchema()
298+
.addProperty("abc", abc)
299+
.addProperty("def", def);
300+
301+
ApiResponse apiResponse = new ApiResponse()
302+
.description("OK")
303+
.content(new Content().addMediaType("application/json",
304+
new MediaType().schema(response)));
305+
306+
openapi.getPaths().addPathItem("/default", new PathItem().get(
307+
new Operation().operationId("apiGetDefault")
308+
.responses(new ApiResponses().addApiResponse("200", apiResponse))));
309+
310+
new InlineModelResolver().flatten(openapi);
311+
312+
// Locate the flattened response model (the only component schema carrying both properties).
313+
Schema responseModel = null;
314+
for (Schema candidate : openapi.getComponents().getSchemas().values()) {
315+
if (candidate.getProperties() != null
316+
&& candidate.getProperties().containsKey("abc")
317+
&& candidate.getProperties().containsKey("def")) {
318+
responseModel = candidate;
319+
break;
320+
}
321+
}
322+
assertNotNull("Flattened response model with abc/def properties must exist", responseModel);
323+
324+
String abcRef = ((Schema) responseModel.getProperties().get("abc")).get$ref();
325+
String defRef = ((Schema) responseModel.getProperties().get("def")).get$ref();
326+
assertNotNull("abc property must be a $ref to a generated schema", abcRef);
327+
assertNotNull("def property must be a $ref to a generated schema", defRef);
328+
assertFalse("abc and def must resolve to DISTINCT schemas, not be merged: " + abcRef,
329+
abcRef.equals(defRef));
330+
}
331+
277332
@Test
278333
public void resolveInlineModelDeduplicatesMultipleRefsToSameExternalFile() {
279334
// Regression test: when the same external schema file is referenced from three separate

0 commit comments

Comments
 (0)