Skip to content

Commit 159b367

Browse files
committed
add implementation to detect schemas matching PageModel shape and replace with generic
1 parent b02ca92 commit 159b367

58 files changed

Lines changed: 4142 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
generatorName: kotlin-spring
2+
outputDir: samples/server/petstore/kotlin-springboot-paged-model
3+
library: spring-boot
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-paged-model.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
6+
additionalProperties:
7+
documentationProvider: none
8+
annotationLibrary: none
9+
useSwaggerUI: "false"
10+
serviceImplementation: "false"
11+
serializableModel: "true"
12+
useBeanValidation: "true"
13+
interfaceOnly: "true"
14+
useSpringBoot3: "true"
15+
substituteGenericPagedModel: "true"
16+
useTags: "true"
17+
requestMappingMode: api_interface
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
generatorName: spring
2+
outputDir: samples/server/petstore/springboot-paged-model
3+
library: spring-boot
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-paged-model.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
6+
additionalProperties:
7+
documentationProvider: none
8+
annotationLibrary: none
9+
useSwaggerUI: "false"
10+
serializableModel: "true"
11+
useBeanValidation: "true"
12+
interfaceOnly: "false"
13+
skipDefaultInterface: "true"
14+
useSpringBoot3: "true"
15+
hideGenerationTimestamp: "true"
16+
substituteGenericPagedModel: "true"
17+
useTags: "true"
18+
requestMappingMode: api_interface

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
103103
public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated";
104104
public static final String GENERATE_SORT_VALIDATION = "generateSortValidation";
105105
public static final String GENERATE_PAGEABLE_CONSTRAINT_VALIDATION = "generatePageableConstraintValidation";
106+
public static final String SUBSTITUTE_GENERIC_PAGED_MODEL = "substituteGenericPagedModel";
106107
public static final String USE_SEALED_RESPONSE_INTERFACES = "useSealedResponseInterfaces";
107108
public static final String COMPANION_OBJECT = "companionObject";
108109

@@ -171,6 +172,7 @@ public String getDescription() {
171172
@Setter private boolean autoXSpringPaginated = false;
172173
@Setter private boolean generateSortValidation = false;
173174
@Setter private boolean generatePageableConstraintValidation = false;
175+
@Setter private boolean substituteGenericPagedModel = false;
174176
@Setter private boolean useSealedResponseInterfaces = false;
175177
@Setter private boolean companionObject = false;
176178

@@ -196,6 +198,9 @@ public String getDescription() {
196198
// Map from operationId to pageable constraints for @ValidPageable annotation generation
197199
private Map<String, SpringPageableScanUtils.PageableConstraintsData> pageableConstraintsRegistry = new HashMap<>();
198200

201+
// Map from schema name to detected paged-model info (populated when substituteGenericPagedModel=true)
202+
private Map<String, PagedModelScanUtils.DetectedPagedModel> pagedModelRegistry = new HashMap<>();
203+
199204
public KotlinSpringServerCodegen() {
200205
super();
201206

@@ -290,6 +295,12 @@ public KotlinSpringServerCodegen() {
290295
addSwitch(AUTO_X_SPRING_PAGINATED, "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected.", autoXSpringPaginated);
291296
addSwitch(GENERATE_SORT_VALIDATION, "Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to the injected Pageable parameter of operations whose 'sort' parameter has enum values. The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. Requires useBeanValidation=true and library=spring-boot.", generateSortValidation);
292297
addSwitch(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library=spring-boot.", generatePageableConstraintValidation);
298+
addSwitch(SUBSTITUTE_GENERIC_PAGED_MODEL,
299+
"Detect schemas that represent paginated responses (an object with a 'content' array property and a "
300+
+ "pagination-metadata property) and replace their generated references with "
301+
+ "org.springframework.data.web.PagedModel<T>. The detected page schemas and the pagination metadata "
302+
+ "schema are suppressed from code generation. Only applies when library=spring-boot.",
303+
substituteGenericPagedModel);
293304
addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject);
294305
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
295306
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
@@ -730,6 +741,10 @@ public void processOpts() {
730741
this.setGeneratePageableConstraintValidation(convertPropertyToBoolean(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION));
731742
}
732743
writePropertyBack(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, generatePageableConstraintValidation);
744+
if (additionalProperties.containsKey(SUBSTITUTE_GENERIC_PAGED_MODEL) && library.equals(SPRING_BOOT)) {
745+
this.setSubstituteGenericPagedModel(convertPropertyToBoolean(SUBSTITUTE_GENERIC_PAGED_MODEL));
746+
}
747+
writePropertyBack(SUBSTITUTE_GENERIC_PAGED_MODEL, substituteGenericPagedModel);
733748
if (isUseSpringBoot3() && isUseSpringBoot4()) {
734749
throw new IllegalArgumentException("Choose between Spring Boot 3 and Spring Boot 4");
735750
}
@@ -1118,6 +1133,28 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
11181133
codegenOperation.allParams.removeIf(param -> param.isQueryParam && defaultPageableQueryParams.contains(param.baseName));
11191134
}
11201135
}
1136+
1137+
// If substituteGenericPagedModel is enabled, replace paged-model return types
1138+
// with org.springframework.data.web.PagedModel<T>.
1139+
if (substituteGenericPagedModel && !pagedModelRegistry.isEmpty()
1140+
&& codegenOperation.returnBaseType != null) {
1141+
PagedModelScanUtils.DetectedPagedModel detected =
1142+
pagedModelRegistry.get(codegenOperation.returnBaseType);
1143+
if (detected != null) {
1144+
String oldType = codegenOperation.returnType;
1145+
String newBaseType = "PagedModel<" + detected.itemSchemaName + ">";
1146+
codegenOperation.returnType = newBaseType;
1147+
codegenOperation.returnBaseType = "PagedModel";
1148+
// Clear any container flag — PagedModel is not itself a List/array
1149+
codegenOperation.returnContainer = null;
1150+
// Remove stale import for the suppressed paged schema and add PagedModel
1151+
codegenOperation.imports.remove(detected.schemaName);
1152+
codegenOperation.imports.add("PagedModel");
1153+
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with PagedModel<{}>",
1154+
codegenOperation.operationId, oldType, detected.itemSchemaName);
1155+
}
1156+
}
1157+
11211158
return codegenOperation;
11221159
}
11231160

@@ -1157,6 +1194,15 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
11571194
}
11581195
}
11591196

1197+
if (SPRING_BOOT.equals(library) && substituteGenericPagedModel) {
1198+
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
1199+
if (!pagedModelRegistry.isEmpty()) {
1200+
importMapping.putIfAbsent("PagedModel", "org.springframework.data.web.PagedModel");
1201+
LOGGER.info("substituteGenericPagedModel: detected {} paged-model schema(s): {}",
1202+
pagedModelRegistry.size(), pagedModelRegistry.keySet());
1203+
}
1204+
}
1205+
11601206
if (!additionalProperties.containsKey(TITLE)) {
11611207
// The purpose of the title is for:
11621208
// - README documentation
@@ -1257,6 +1303,41 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
12571303
}
12581304
}
12591305

1306+
@Override
1307+
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
1308+
objs = super.postProcessAllModels(objs);
1309+
1310+
if (substituteGenericPagedModel && !pagedModelRegistry.isEmpty()) {
1311+
// Collect the pagination metadata schema names to suppress (deduplicated)
1312+
Set<String> metaSchemasToSuppress = new HashSet<>();
1313+
for (PagedModelScanUtils.DetectedPagedModel detected : pagedModelRegistry.values()) {
1314+
if (detected.metaSchemaName != null) {
1315+
metaSchemasToSuppress.add(detected.metaSchemaName);
1316+
}
1317+
}
1318+
1319+
// Suppress each detected paged schema
1320+
for (Map.Entry<String, PagedModelScanUtils.DetectedPagedModel> entry : pagedModelRegistry.entrySet()) {
1321+
String schemaName = entry.getKey();
1322+
PagedModelScanUtils.DetectedPagedModel detected = entry.getValue();
1323+
if (objs.remove(schemaName) != null) {
1324+
LOGGER.info("substituteGenericPagedModel: suppressing model '{}' — replaced by PagedModel<{}>",
1325+
schemaName, detected.itemSchemaName);
1326+
}
1327+
}
1328+
1329+
// Suppress the pagination metadata schema(s)
1330+
for (String metaName : metaSchemasToSuppress) {
1331+
if (objs.remove(metaName) != null) {
1332+
LOGGER.info("substituteGenericPagedModel: suppressing pagination metadata model '{}'"
1333+
+ " — replaced by PagedModel.PageMetadata", metaName);
1334+
}
1335+
}
1336+
}
1337+
1338+
return objs;
1339+
}
1340+
12601341
@Override
12611342
public ModelsMap postProcessModelsEnum(ModelsMap objs) {
12621343
objs = super.postProcessModelsEnum(objs);

0 commit comments

Comments
 (0)