Skip to content

Commit 3a6c41c

Browse files
committed
revert default-resolution related changes as the behavior is unspecified and therefore dangerous
1 parent a00b0a7 commit 3a6c41c

11 files changed

Lines changed: 8 additions & 424 deletions

File tree

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,14 +1932,8 @@ protected void ensureInheritanceForDiscriminatorMapping(Schema parent, Schema ch
19321932
// already done, so no need to add
19331933
return;
19341934
}
1935-
// Prepend at position 0 so the parent ref is the FIRST item in the child's allOf.
1936-
// resolveDefault() uses last-writer-wins semantics, so anything the child already
1937-
// expressed in its allOf (including its own defaults) must come after the base in
1938-
// order to win. Appending at the end would make the parent the last — and therefore
1939-
// the winning — default, which is wrong: the normalizer is injecting structural
1940-
// inheritance here, not overriding child-specified defaults.
19411935
Schema refToParent = new Schema<>().$ref(reference);
1942-
allOf.add(0, refToParent);
1936+
allOf.add(refToParent);
19431937
} else {
19441938
allOf = new ArrayList<>();
19451939
child.setAllOf(allOf);

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,13 @@ public static Map<String, PageableDefaultsData> scanPageableDefaults(
535535
if (schema == null) {
536536
continue;
537537
}
538-
Object defaultValue = ModelUtils.resolveDefault(openAPI, schema);
539-
if (defaultValue == null) {
538+
if (schema.get$ref() != null) {
539+
schema = ModelUtils.getReferencedSchema(openAPI, schema);
540+
}
541+
if (schema == null || schema.getDefault() == null) {
540542
continue;
541543
}
544+
Object defaultValue = schema.getDefault();
542545
switch (param.getName()) {
543546
case PAGE:
544547
if (defaultValue instanceof Number) {

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

Lines changed: 0 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,78 +1020,6 @@ public static ResolvedMinBound resolveMinimumBound(OpenAPI openAPI, Schema<?> sc
10201020
.reduce(result, ResolvedMinBound::getLargerMinBound);
10211021
}
10221022

1023-
/**
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>
1052-
*
1053-
* @param openAPI the OpenAPI document used to resolve {@code $ref}s
1054-
* @param schema the schema to inspect
1055-
* @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.
1060-
*/
1061-
public static Object resolveDefault(OpenAPI openAPI, Schema<?> schema) {
1062-
schema = getReferencedSchema(openAPI, schema);
1063-
if (schema == null) return null;
1064-
1065-
// Direct default short-circuits — no allOf traversal needed.
1066-
Object directDefault = schema.getDefault();
1067-
if (directDefault != null) {
1068-
return directDefault;
1069-
}
1070-
1071-
// Walk allOf top-to-bottom; each non-null result overwrites the previous candidate.
1072-
if (hasAllOf(schema)) {
1073-
return getLastNonNullDefault(openAPI, schema);
1074-
}
1075-
return null;
1076-
}
1077-
1078-
/**
1079-
* Walks {@code allOf} items top-to-bottom and returns the <em>last</em> non-null
1080-
* {@code default} value found (last-writer-wins). Each item is fully resolved
1081-
* (including its own nested {@code allOf}) before the candidate is updated, so
1082-
* arbitrarily deep chains are flattened correctly.
1083-
*/
1084-
private static Object getLastNonNullDefault(OpenAPI openAPI, Schema<?> schema) {
1085-
Object last = null;
1086-
for (Schema<?> item : schema.getAllOf()) {
1087-
Object resolved = resolveDefault(openAPI, item);
1088-
if (resolved != null) {
1089-
last = resolved;
1090-
}
1091-
}
1092-
return last;
1093-
}
1094-
10951023
public static boolean hasValidation(Schema sc) {
10961024
return (
10971025
sc.getMaxItems() != null ||

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

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,62 +1844,4 @@ public void oneOf_issue_23276() {
18441844
assertNotNull(payload.getOneOf());
18451845
}
18461846

1847-
@Test
1848-
public void ensureInheritanceForDiscriminatorMapping_prependsParentRef_childDefaultWins() {
1849-
// When a child already has allOf items (e.g. a base schema carrying "base_default"),
1850-
// the normalizer must INSERT the parent $ref at position 0 — not append at the end.
1851-
// With last-writer-wins semantics in resolveDefault(), appending at the end would make
1852-
// the parent's default the winner, incorrectly overriding the child's own default.
1853-
OpenAPI openAPI = TestUtils.createOpenAPI();
1854-
1855-
Schema<?> parent = new StringSchema();
1856-
parent.setDefault("parent_default");
1857-
1858-
Schema<?> base = new StringSchema();
1859-
base.setDefault("base_default");
1860-
openAPI.getComponents().addSchemas("Base", base);
1861-
1862-
// Child: allOf → [Base]; no Parent ref yet — use mutable list (parser always produces ArrayList)
1863-
Schema<?> child = new Schema<>().allOf(new ArrayList<>(List.of(new Schema<>().$ref("#/components/schemas/Base"))));
1864-
openAPI.getComponents().addSchemas("Child", child);
1865-
openAPI.getComponents().addSchemas("Parent", parent);
1866-
1867-
OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, Map.of());
1868-
normalizer.ensureInheritanceForDiscriminatorMapping(parent, child, "Parent", new HashSet<>());
1869-
1870-
// Parent ref must be prepended at index 0; Base ref stays at index 1
1871-
assertEquals(((Schema<?>) child.getAllOf().get(0)).get$ref(), "#/components/schemas/Parent");
1872-
assertEquals(((Schema<?>) child.getAllOf().get(1)).get$ref(), "#/components/schemas/Base");
1873-
1874-
// resolveDefault walks top-to-bottom, last non-null wins → "base_default"
1875-
assertEquals(ModelUtils.resolveDefault(openAPI, child), "base_default");
1876-
}
1877-
1878-
@Test
1879-
public void ensureInheritanceForDiscriminatorMapping_noExistingAllOf_noProperties_directDefaultPreserved() {
1880-
// When a child has no allOf and no properties but does have a direct default,
1881-
// the normalizer creates allOf with just the parent $ref. The child's direct default
1882-
// must NOT be cleared — resolveDefault() short-circuits on it before inspecting allOf.
1883-
OpenAPI openAPI = TestUtils.createOpenAPI();
1884-
1885-
Schema<?> parent = new StringSchema();
1886-
parent.setDefault("parent_default");
1887-
openAPI.getComponents().addSchemas("Parent", parent);
1888-
1889-
// Child: direct default only, no allOf, no properties
1890-
Schema<?> child = new StringSchema();
1891-
child.setDefault("child_direct_default");
1892-
openAPI.getComponents().addSchemas("Child", child);
1893-
1894-
OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, Map.of());
1895-
normalizer.ensureInheritanceForDiscriminatorMapping(parent, child, "Parent", new HashSet<>());
1896-
1897-
// Child now has allOf (parent ref only), and still holds its direct default
1898-
assertNotNull(child.getAllOf());
1899-
assertEquals(child.getDefault(), "child_direct_default");
1900-
1901-
// resolveDefault short-circuits on the direct default — child wins
1902-
assertEquals(ModelUtils.resolveDefault(openAPI, child), "child_direct_default");
1903-
}
1904-
19051847
}

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

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7366,25 +7366,6 @@ public void generatePageableConstraintValidationResolvesMinimumFromAllOfRef() th
73667366
.containsWithNameAndAttributes("ValidPageable", Map.of("minSize", "5"));
73677367
}
73687368

7369-
@Test
7370-
public void scanPageableDefaultsResolvesDefaultFromAllOfRef() throws IOException {
7371-
Map<String, Object> props = new HashMap<>();
7372-
props.put(SpringCodegen.INTERFACE_ONLY, "true");
7373-
props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
7374-
props.put(SpringCodegen.USE_TAGS, "true");
7375-
props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
7376-
7377-
Map<String, File> files = generateFromContract(
7378-
"src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
7379-
7380-
// findPetsWithDefaultFromAllOfRef: default: 7 is on the referenced schema only
7381-
JavaFileAssert.assertThat(files.get("PetApi.java"))
7382-
.assertMethod("findPetsWithDefaultFromAllOfRef")
7383-
.assertParameter("pageable")
7384-
.assertParameterAnnotations()
7385-
.containsWithNameAndAttributes("PageableDefault", Map.of("size", "7"));
7386-
}
7387-
73887369
// -------------------------------------------------------------------------
73897370
// @PageableDefault / @SortDefault tests
73907371
// -------------------------------------------------------------------------

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4312,20 +4312,6 @@ public void generatePageableConstraintValidationResolvesMinimumFromAllOfRef() th
43124312
"@ValidPageable(minSize = 5) should be resolved from allOf $ref schema");
43134313
}
43144314

4315-
@Test
4316-
public void scanPageableDefaultsResolvesDefaultFromAllOfRef() throws Exception {
4317-
Map<String, Object> additionalProperties = new HashMap<>();
4318-
additionalProperties.put(USE_TAGS, "true");
4319-
additionalProperties.put(INTERFACE_ONLY, "true");
4320-
additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
4321-
4322-
Map<String, File> files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
4323-
4324-
File petApi = files.get("PetApi.kt");
4325-
// findPetsWithDefaultFromAllOfRef: default: 7 is on the referenced schema only
4326-
assertFileContains(petApi.toPath(), "@PageableDefault(size = 7)");
4327-
}
4328-
43294315
// ========== AUTO X-SPRING-PAGINATED TESTS ==========
43304316

43314317
@Test

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

Lines changed: 0 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,167 +1181,4 @@ public void resolveMinimumBound_nestedAllOf_mostRestrictiveAcrossAllLevels() {
11811181
assertEquals(bound.minBound, BigDecimal.valueOf(20));
11821182
}
11831183

1184-
// -------------------------------------------------------------------------
1185-
// resolveDefault
1186-
// -------------------------------------------------------------------------
1187-
1188-
@Test
1189-
public void resolveDefault_nullSchema_returnsNull() {
1190-
assertNull(ModelUtils.resolveDefault(new OpenAPI(), null));
1191-
}
1192-
1193-
@Test
1194-
public void resolveDefault_noDefaultDefined_returnsNull() {
1195-
OpenAPI openAPI = TestUtils.createOpenAPI();
1196-
assertNull(ModelUtils.resolveDefault(openAPI, new IntegerSchema()));
1197-
}
1198-
1199-
@Test
1200-
public void resolveDefault_inlineDefault_returnsIt() {
1201-
OpenAPI openAPI = TestUtils.createOpenAPI();
1202-
Schema<?> schema = new IntegerSchema();
1203-
schema.setDefault(10);
1204-
assertEquals(ModelUtils.resolveDefault(openAPI, schema), 10);
1205-
}
1206-
1207-
@Test
1208-
public void resolveDefault_refToSchemaWithDefault_resolvesRef() {
1209-
OpenAPI openAPI = TestUtils.createOpenAPI();
1210-
Schema<?> refTarget = new IntegerSchema();
1211-
refTarget.setDefault(0);
1212-
openAPI.getComponents().addSchemas("MyInt", refTarget);
1213-
1214-
Schema<?> ref = new Schema<>().$ref("#/components/schemas/MyInt");
1215-
assertEquals(ModelUtils.resolveDefault(openAPI, ref), 0);
1216-
}
1217-
1218-
@Test
1219-
public void resolveDefault_allOfItemHasDefault_returnsIt() {
1220-
OpenAPI openAPI = TestUtils.createOpenAPI();
1221-
Schema<?> allOfItem = new IntegerSchema();
1222-
allOfItem.setDefault(20);
1223-
openAPI.getComponents().addSchemas("Base", allOfItem);
1224-
1225-
// Inline schema has no default; allOf item has default=20
1226-
Schema<?> schema = new Schema<>().allOf(List.of(new Schema<>().$ref("#/components/schemas/Base")));
1227-
assertEquals(ModelUtils.resolveDefault(openAPI, schema), 20);
1228-
}
1229-
1230-
@Test
1231-
public void resolveDefault_inlineDefaultTakesPrecedenceOverAllOf() {
1232-
OpenAPI openAPI = TestUtils.createOpenAPI();
1233-
Schema<?> allOfItem = new IntegerSchema();
1234-
allOfItem.setDefault(99);
1235-
openAPI.getComponents().addSchemas("Base", allOfItem);
1236-
1237-
// Inline schema default=5 should win over allOf item default=99
1238-
Schema<?> schema = new IntegerSchema();
1239-
schema.setDefault(5);
1240-
schema.setAllOf(List.of(new Schema<>().$ref("#/components/schemas/Base")));
1241-
assertEquals(ModelUtils.resolveDefault(openAPI, schema), 5);
1242-
}
1243-
1244-
@Test
1245-
public void resolveDefault_allOfItemsNoDefault_returnsNull() {
1246-
OpenAPI openAPI = TestUtils.createOpenAPI();
1247-
openAPI.getComponents().addSchemas("Base", new IntegerSchema()); // no default
1248-
1249-
Schema<?> schema = new Schema<>().allOf(List.of(new Schema<>().$ref("#/components/schemas/Base")));
1250-
assertNull(ModelUtils.resolveDefault(openAPI, schema));
1251-
}
1252-
1253-
@Test
1254-
public void resolveDefault_stringDefault_returnsIt() {
1255-
OpenAPI openAPI = TestUtils.createOpenAPI();
1256-
Schema<?> schema = new StringSchema();
1257-
schema.setDefault("hello");
1258-
assertEquals(ModelUtils.resolveDefault(openAPI, schema), "hello");
1259-
}
1260-
1261-
@Test
1262-
public void resolveDefault_nestedAllOf_findsDefaultInNestedItem() {
1263-
OpenAPI openAPI = TestUtils.createOpenAPI();
1264-
// base has default=99; mid allOf → base; top allOf → mid
1265-
Schema<?> base = new IntegerSchema();
1266-
base.setDefault(99);
1267-
openAPI.getComponents().addSchemas("Base", base);
1268-
1269-
Schema<?> mid = new Schema<>().allOf(List.of(new Schema<>().$ref("#/components/schemas/Base")));
1270-
openAPI.getComponents().addSchemas("Mid", mid);
1271-
1272-
Schema<?> top = new Schema<>().allOf(List.of(new Schema<>().$ref("#/components/schemas/Mid")));
1273-
1274-
assertEquals(ModelUtils.resolveDefault(openAPI, top), 99);
1275-
}
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-
}
13471184
}

samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ interface PetApi {
110110
value = [PATH_FIND_PETS_WITH_DEFAULT_FROM_ALL_OF_REF],
111111
produces = ["application/json"]
112112
)
113-
fun findPetsWithDefaultFromAllOfRef(@PageableDefault(size = 7) pageable: Pageable): ResponseEntity<List<Pet>> {
113+
fun findPetsWithDefaultFromAllOfRef(pageable: Pageable): ResponseEntity<List<Pet>> {
114114
return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
115115
}
116116

0 commit comments

Comments
 (0)