Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -862,9 +862,20 @@ private String matchGenerated(Schema model) {
}
// Structural match: compare with volatile fields stripped at every level.
// See generatedStructuralSignature field for a full explanation of why this is needed.
String structural = computeStructuralSignature(model);
if (generatedStructuralSignature.containsKey(structural)) {
return generatedStructuralSignature.get(structural);
//
// Only applied to *titled* schemas. A title denotes a named type that should be reused
// wherever it appears, so parser-induced volatile differences (description, type,
// example) must not split it into numbered duplicates. Anonymous/untitled inline
// schemas, however, may be intentionally distinct even when structurally identical once
// those volatile fields are stripped (e.g. two response properties that differ only by
// description) — unifying them silently changes the generated type of one property and
// breaks user code. This mirrors the titled-only guards in flatten() pre-population and
// deduplicateComponents().
if (model.getTitle() != null) {
String structural = computeStructuralSignature(model);
if (generatedStructuralSignature.containsKey(structural)) {
return generatedStructuralSignature.get(structural);
}
}
} catch (JsonProcessingException e) {
e.printStackTrace();
Expand All @@ -876,7 +887,11 @@ private String matchGenerated(Schema model) {
private void addGenerated(String name, Schema model) {
try {
generatedSignature.put(structureMapper.writeValueAsString(model), name);
generatedStructuralSignature.putIfAbsent(computeStructuralSignature(model), name);
// Only register the volatile-stripped structural signature for titled schemas; untitled
// inline schemas must not participate in the structural-match fallback (see matchGenerated).
if (model.getTitle() != null) {
generatedStructuralSignature.putIfAbsent(computeStructuralSignature(model), name);
}
} catch (JsonProcessingException e) {
e.printStackTrace();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,61 @@ public void resolveInlineModelDeduplicatesWhenParserMutatesPropertyDescriptions(
assertNull("Duplicate Widget_1 must not exist — shape-fingerprint dedup must fire", schemas.get("Widget_1"));
}

@Test
public void resolveInlineModelKeepsUntitledSchemasDifferingOnlyByDescriptionDistinct() {
// Regression test for #24004: two distinct *untitled* inline object schemas that differ
// only in their descriptions (here the nested 'result' property: "ABC Result" vs
// "DEF Result") must NOT be merged. The volatile-stripped structural-signature fallback in
// matchGenerated() collapses them once description/type are removed; that fallback is only
// intended to unify titled named types across parser volatility, so it must not fire for
// untitled inline schemas — otherwise 'def' silently gets the type generated for 'abc' and
// breaks user code that expects two separate types (regression introduced in 7.23).
OpenAPI openapi = new OpenAPI();
openapi.setComponents(new Components());
openapi.setPaths(new Paths());

Schema abc = new ObjectSchema()
.description("first container")
.addProperty("result", new StringSchema().description("ABC Result"));
Schema def = new ObjectSchema()
.description("second container")
.addProperty("result", new StringSchema().description("DEF Result"));

Schema response = new ObjectSchema()
.addProperty("abc", abc)
.addProperty("def", def);

ApiResponse apiResponse = new ApiResponse()
.description("OK")
.content(new Content().addMediaType("application/json",
new MediaType().schema(response)));

openapi.getPaths().addPathItem("/default", new PathItem().get(
new Operation().operationId("apiGetDefault")
.responses(new ApiResponses().addApiResponse("200", apiResponse))));

new InlineModelResolver().flatten(openapi);

// Locate the flattened response model (the only component schema carrying both properties).
Schema responseModel = null;
for (Schema candidate : openapi.getComponents().getSchemas().values()) {
if (candidate.getProperties() != null
&& candidate.getProperties().containsKey("abc")
&& candidate.getProperties().containsKey("def")) {
responseModel = candidate;
break;
}
}
assertNotNull("Flattened response model with abc/def properties must exist", responseModel);

String abcRef = ((Schema) responseModel.getProperties().get("abc")).get$ref();
String defRef = ((Schema) responseModel.getProperties().get("def")).get$ref();
assertNotNull("abc property must be a $ref to a generated schema", abcRef);
assertNotNull("def property must be a $ref to a generated schema", defRef);
assertFalse("abc and def must resolve to DISTINCT schemas, not be merged: " + abcRef,
abcRef.equals(defRef));
}

@Test
public void resolveInlineModelDeduplicatesMultipleRefsToSameExternalFile() {
// Regression test: when the same external schema file is referenced from three separate
Expand Down
Loading