Skip to content

Commit 94ed6c5

Browse files
committed
fix: ensure correct parent reference ordering in allOf for default resolution
1 parent 90282ea commit 94ed6c5

3 files changed

Lines changed: 69 additions & 1 deletion

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1757,8 +1757,14 @@ protected void ensureInheritanceForDiscriminatorMapping(Schema parent, Schema ch
17571757
// already done, so no need to add
17581758
return;
17591759
}
1760+
// Prepend at position 0 so the parent ref is the FIRST item in the child's allOf.
1761+
// resolveDefault() uses last-writer-wins semantics, so anything the child already
1762+
// expressed in its allOf (including its own defaults) must come after the base in
1763+
// order to win. Appending at the end would make the parent the last — and therefore
1764+
// the winning — default, which is wrong: the normalizer is injecting structural
1765+
// inheritance here, not overriding child-specified defaults.
17601766
Schema refToParent = new Schema<>().$ref(reference);
1761-
allOf.add(refToParent);
1767+
allOf.add(0, refToParent);
17621768
} else {
17631769
allOf = new ArrayList<>();
17641770
child.setAllOf(allOf);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,10 @@ public static ResolvedMinBound resolveMinimumBound(OpenAPI openAPI, Schema<?> sc
10531053
* @param openAPI the OpenAPI document used to resolve {@code $ref}s
10541054
* @param schema the schema to inspect
10551055
* @return the effective default value, or {@code null} if none is defined
1056+
* @implNote The result depends on the {@code allOf} array ordering <em>as it exists when
1057+
* this method is called</em> — i.e., after the OpenAPI normalizer has run.
1058+
* Normalizer mutations (e.g. {@code ensureInheritanceForDiscriminatorMapping} prepending
1059+
* a parent {@code $ref}) are therefore visible here and are accounted for by design.
10561060
*/
10571061
public static Object resolveDefault(OpenAPI openAPI, Schema<?> schema) {
10581062
schema = getReferencedSchema(openAPI, schema);

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,4 +1544,62 @@ public void oneOf_issue_23276() {
15441544
assertNotNull(payload.getOneOf());
15451545
}
15461546

1547+
@Test
1548+
public void ensureInheritanceForDiscriminatorMapping_prependsParentRef_childDefaultWins() {
1549+
// When a child already has allOf items (e.g. a base schema carrying "base_default"),
1550+
// the normalizer must INSERT the parent $ref at position 0 — not append at the end.
1551+
// With last-writer-wins semantics in resolveDefault(), appending at the end would make
1552+
// the parent's default the winner, incorrectly overriding the child's own default.
1553+
OpenAPI openAPI = TestUtils.createOpenAPI();
1554+
1555+
Schema<?> parent = new StringSchema();
1556+
parent.setDefault("parent_default");
1557+
1558+
Schema<?> base = new StringSchema();
1559+
base.setDefault("base_default");
1560+
openAPI.getComponents().addSchemas("Base", base);
1561+
1562+
// Child: allOf → [Base]; no Parent ref yet — use mutable list (parser always produces ArrayList)
1563+
Schema<?> child = new Schema<>().allOf(new ArrayList<>(List.of(new Schema<>().$ref("#/components/schemas/Base"))));
1564+
openAPI.getComponents().addSchemas("Child", child);
1565+
openAPI.getComponents().addSchemas("Parent", parent);
1566+
1567+
OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, Map.of());
1568+
normalizer.ensureInheritanceForDiscriminatorMapping(parent, child, "Parent", new HashSet<>());
1569+
1570+
// Parent ref must be prepended at index 0; Base ref stays at index 1
1571+
assertEquals(((Schema<?>) child.getAllOf().get(0)).get$ref(), "#/components/schemas/Parent");
1572+
assertEquals(((Schema<?>) child.getAllOf().get(1)).get$ref(), "#/components/schemas/Base");
1573+
1574+
// resolveDefault walks top-to-bottom, last non-null wins → "base_default"
1575+
assertEquals(ModelUtils.resolveDefault(openAPI, child), "base_default");
1576+
}
1577+
1578+
@Test
1579+
public void ensureInheritanceForDiscriminatorMapping_noExistingAllOf_noProperties_directDefaultPreserved() {
1580+
// When a child has no allOf and no properties but does have a direct default,
1581+
// the normalizer creates allOf with just the parent $ref. The child's direct default
1582+
// must NOT be cleared — resolveDefault() short-circuits on it before inspecting allOf.
1583+
OpenAPI openAPI = TestUtils.createOpenAPI();
1584+
1585+
Schema<?> parent = new StringSchema();
1586+
parent.setDefault("parent_default");
1587+
openAPI.getComponents().addSchemas("Parent", parent);
1588+
1589+
// Child: direct default only, no allOf, no properties
1590+
Schema<?> child = new StringSchema();
1591+
child.setDefault("child_direct_default");
1592+
openAPI.getComponents().addSchemas("Child", child);
1593+
1594+
OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, Map.of());
1595+
normalizer.ensureInheritanceForDiscriminatorMapping(parent, child, "Parent", new HashSet<>());
1596+
1597+
// Child now has allOf (parent ref only), and still holds its direct default
1598+
assertNotNull(child.getAllOf());
1599+
assertEquals(child.getDefault(), "child_direct_default");
1600+
1601+
// resolveDefault short-circuits on the direct default — child wins
1602+
assertEquals(ModelUtils.resolveDefault(openAPI, child), "child_direct_default");
1603+
}
1604+
15471605
}

0 commit comments

Comments
 (0)