Skip to content

Commit 90282ea

Browse files
committed
fix: update default resolution logic for allOf schemas to last-writer-wins semantics
1 parent 011c7b7 commit 90282ea

2 files changed

Lines changed: 119 additions & 28 deletions

File tree

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

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,21 +1021,34 @@ public static ResolvedMinBound resolveMinimumBound(OpenAPI openAPI, Schema<?> sc
10211021
}
10221022

10231023
/**
1024-
* Returns the effective {@code default} for the given schema, resolving through a top-level
1025-
* {@code $ref} and recursively walking any {@code allOf} items.
1026-
* The inline schema's default takes precedence (explicit per-endpoint override);
1027-
* falls back to the first non-null default found via depth-first search of {@code allOf} items.
1028-
* Circular {@code allOf} references are detected and skipped.
1029-
*
1030-
* @param openAPI the OpenAPI document used to resolve {@code $ref}s
1031-
* @param schema the schema to inspect
1032-
* @return the effective default value, or {@code null} if none is defined
1033-
*/
1034-
/**
1035-
* Returns the effective {@code default} for the given schema, resolving through a top-level
1036-
* {@code $ref} and recursively walking any {@code allOf} items.
1037-
* The inline schema's default takes precedence (explicit per-endpoint override);
1038-
* falls back to the first non-null default found via depth-first search of {@code allOf} items.
1024+
* Returns the effective {@code default} for the given schema using
1025+
* <em>last-writer-wins</em> semantics across the flattened {@code allOf} chain.
1026+
*
1027+
* <h3>Resolution algorithm</h3>
1028+
* <ol>
1029+
* <li>Resolve any top-level {@code $ref} to obtain the concrete schema.</li>
1030+
* <li>If the schema has a direct {@code default} (i.e. the {@code default:} key at
1031+
* the same level as {@code allOf:}), return it immediately — no traversal needed.</li>
1032+
* <li>Otherwise walk the {@code allOf} array <strong>top-to-bottom</strong>. Each
1033+
* item is itself fully resolved (recursing into nested {@code allOf} chains), and
1034+
* its result — if non-null — overwrites the current candidate (last-writer-wins).</li>
1035+
* </ol>
1036+
*
1037+
* <h3>Example</h3>
1038+
* <pre>{@code
1039+
* # Base1: default = "base_1" → Step 1: candidate = "base_1"
1040+
* # Base2: default = "base_2" → Step 2: candidate = "base_2" (overwrites Step 1)
1041+
* #
1042+
* # Intermediate:
1043+
* # allOf: [$ref Base1, $ref Base2]
1044+
* # → resolves to "base_2" (Base2 is last, wins over Base1)
1045+
* #
1046+
* # Final:
1047+
* # allOf:
1048+
* # - $ref: Intermediate → Step 3a: candidate = "base_2"
1049+
* # - default: "final" → Step 3b: candidate = "final" (overwrites Step 3a)
1050+
* # → resolves to "final"
1051+
* }</pre>
10391052
*
10401053
* @param openAPI the OpenAPI document used to resolve {@code $ref}s
10411054
* @param schema the schema to inspect
@@ -1045,27 +1058,34 @@ public static Object resolveDefault(OpenAPI openAPI, Schema<?> schema) {
10451058
schema = getReferencedSchema(openAPI, schema);
10461059
if (schema == null) return null;
10471060

1048-
Object defaultValue = schema.getDefault();
1049-
if (defaultValue != null) {
1050-
// inline default value takes precedence
1051-
return defaultValue;
1061+
// Direct default short-circuits — no allOf traversal needed.
1062+
Object directDefault = schema.getDefault();
1063+
if (directDefault != null) {
1064+
return directDefault;
10521065
}
1066+
1067+
// Walk allOf top-to-bottom; each non-null result overwrites the previous candidate.
10531068
if (hasAllOf(schema)) {
1054-
return getFirstNonNullDefault(openAPI, schema).orElse(null);
1069+
return getLastNonNullDefault(openAPI, schema);
10551070
}
10561071
return null;
10571072
}
10581073

10591074
/**
1060-
* Recursively searches {@code allOf} items for the first non-null {@code default} value.
1061-
* The first non-null default in {@code allOf} wins.
1062-
* This behavior is arbitrary and may not reflect intentions of the spec creator, as default's inheritance/overriding behavior is undefined.
1075+
* Walks {@code allOf} items top-to-bottom and returns the <em>last</em> non-null
1076+
* {@code default} value found (last-writer-wins). Each item is fully resolved
1077+
* (including its own nested {@code allOf}) before the candidate is updated, so
1078+
* arbitrarily deep chains are flattened correctly.
10631079
*/
1064-
private static @NonNull Optional<Object> getFirstNonNullDefault(OpenAPI openAPI, Schema<?> schema) {
1065-
return schema.getAllOf().stream()
1066-
.map(item -> resolveDefault(openAPI, item))
1067-
.filter(Objects::nonNull)
1068-
.findFirst();
1080+
private static Object getLastNonNullDefault(OpenAPI openAPI, Schema<?> schema) {
1081+
Object last = null;
1082+
for (Schema<?> item : schema.getAllOf()) {
1083+
Object resolved = resolveDefault(openAPI, item);
1084+
if (resolved != null) {
1085+
last = resolved;
1086+
}
1087+
}
1088+
return last;
10691089
}
10701090

10711091
public static boolean hasValidation(Schema sc) {

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,4 +1273,75 @@ public void resolveDefault_nestedAllOf_findsDefaultInNestedItem() {
12731273

12741274
assertEquals(ModelUtils.resolveDefault(openAPI, top), 99);
12751275
}
1276+
1277+
@Test
1278+
public void resolveDefault_allOf_lastDefaultWins() {
1279+
// allOf: [Base1(default="base_1"), Base2(default="base_2")]
1280+
// Base2 is last → wins
1281+
OpenAPI openAPI = TestUtils.createOpenAPI();
1282+
Schema<?> base1 = new StringSchema();
1283+
base1.setDefault("base_1");
1284+
openAPI.getComponents().addSchemas("Base1", base1);
1285+
1286+
Schema<?> base2 = new StringSchema();
1287+
base2.setDefault("base_2");
1288+
openAPI.getComponents().addSchemas("Base2", base2);
1289+
1290+
Schema<?> schema = new Schema<>().allOf(List.of(
1291+
new Schema<>().$ref("#/components/schemas/Base1"),
1292+
new Schema<>().$ref("#/components/schemas/Base2")
1293+
));
1294+
assertEquals(ModelUtils.resolveDefault(openAPI, schema), "base_2");
1295+
}
1296+
1297+
@Test
1298+
public void resolveDefault_allOf_lastNonNullDefaultWins() {
1299+
// allOf: [Base1(default="base_1"), NoDefault] — trailing null item does not clear candidate
1300+
OpenAPI openAPI = TestUtils.createOpenAPI();
1301+
Schema<?> base1 = new StringSchema();
1302+
base1.setDefault("base_1");
1303+
openAPI.getComponents().addSchemas("Base1", base1);
1304+
1305+
Schema<?> schema = new Schema<>().allOf(List.of(
1306+
new Schema<>().$ref("#/components/schemas/Base1"),
1307+
new StringSchema() // no default
1308+
));
1309+
assertEquals(ModelUtils.resolveDefault(openAPI, schema), "base_1");
1310+
}
1311+
1312+
@Test
1313+
public void resolveDefault_fullChain_lastWriterWins() {
1314+
// Mirrors the documented resolution example:
1315+
// Base1: default="base_1"
1316+
// Base2: default="base_2"
1317+
// Intermediate: allOf: [Base1, Base2] → resolves to "base_2" (Base2 is last)
1318+
// Final: allOf: [Intermediate, {default:"final"}] → resolves to "final" (inline patch is last)
1319+
OpenAPI openAPI = TestUtils.createOpenAPI();
1320+
1321+
Schema<?> base1 = new StringSchema();
1322+
base1.setDefault("base_1");
1323+
openAPI.getComponents().addSchemas("Base1", base1);
1324+
1325+
Schema<?> base2 = new StringSchema();
1326+
base2.setDefault("base_2");
1327+
openAPI.getComponents().addSchemas("Base2", base2);
1328+
1329+
Schema<?> intermediate = new Schema<>().allOf(List.of(
1330+
new Schema<>().$ref("#/components/schemas/Base1"),
1331+
new Schema<>().$ref("#/components/schemas/Base2")
1332+
));
1333+
openAPI.getComponents().addSchemas("Intermediate", intermediate);
1334+
1335+
Schema<?> inlinePatch = new StringSchema();
1336+
inlinePatch.setDefault("final");
1337+
Schema<?> finalSchema = new Schema<>().allOf(List.of(
1338+
new Schema<>().$ref("#/components/schemas/Intermediate"),
1339+
inlinePatch
1340+
));
1341+
1342+
// Intermediate alone resolves to "base_2"
1343+
assertEquals(ModelUtils.resolveDefault(openAPI, new Schema<>().$ref("#/components/schemas/Intermediate")), "base_2");
1344+
// Full chain resolves to "final"
1345+
assertEquals(ModelUtils.resolveDefault(openAPI, finalSchema), "final");
1346+
}
12761347
}

0 commit comments

Comments
 (0)