Skip to content

Commit b063a67

Browse files
authored
[JAVA-SPRING;KOTLIN-SPRING] bugfix - PagedModel<T> - default to custom DTO; allow override via import mapping and schema mapping; feature - allow for declarative http interfaces (#23601)
* fix(spring): ensure PagedModel uses mapped FQN for item type in code generation fix(spring): enhance PagedModel handling with import mapping and schema mapping support * fix(spring): generate supporting PagedModel files in config package (can be replaced with custom one via importMapping) * update samples * improve test add support for spring declarative interface * update documentation * update documentation * update documentation * update documentation * implement feedback from CR
1 parent 209f7d1 commit b063a67

19 files changed

Lines changed: 580 additions & 38 deletions

File tree

docs/generators/java-camel.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
104104
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
105105
|sourceFolder|source folder for generated code| |src/main/java|
106106
|springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null|
107-
|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a pagination-metadata property) and replace their generated references with org.springframework.data.web.PagedModel&lt;T&gt;. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot.| |false|
107+
|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel&lt;T&gt;. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot or spring-http-interface.| |false|
108108
|testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi|
109109
|title|server title name or client service name| |OpenAPI Spring|
110110
|unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false|

docs/generators/kotlin-spring.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
5858
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |null|
5959
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |null|
6060
|sourceFolder|source folder for generated code| |src/main/kotlin|
61-
|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a pagination-metadata property) and replace their generated references with org.springframework.data.web.PagedModel&lt;T&gt;. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot.| |false|
61+
|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel&lt;T&gt;. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot or spring-declarative-http-interface.| |false|
6262
|title|server title name or client service name| |OpenAPI Kotlin Spring|
6363
|useBeanValidation|Use BeanValidation API annotations to validate data types| |true|
6464
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|

docs/generators/spring.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
9797
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
9898
|sourceFolder|source folder for generated code| |src/main/java|
9999
|springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null|
100-
|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a pagination-metadata property) and replace their generated references with org.springframework.data.web.PagedModel&lt;T&gt;. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot.| |false|
100+
|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel&lt;T&gt;. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot or spring-http-interface.| |false|
101101
|testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi|
102102
|title|server title name or client service name| |OpenAPI Spring|
103103
|unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false|

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

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ public String getDescription() {
200200

201201
// Map from schema name to detected paged-model info (populated when substituteGenericPagedModel=true)
202202
private Map<String, PagedModelScanUtils.DetectedPagedModel> pagedModelRegistry = new HashMap<>();
203+
// Simple class name of the PagedModel substitute (derived from importMapping; defaults to "PagedModel")
204+
private String pagedModelClassName = "PagedModel";
203205

204206
public KotlinSpringServerCodegen() {
205207
super();
@@ -296,10 +298,10 @@ public KotlinSpringServerCodegen() {
296298
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);
297299
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);
298300
addSwitch(SUBSTITUTE_GENERIC_PAGED_MODEL,
299-
"Detect schemas that represent paginated responses (an object with a 'content' array property and a "
301+
"Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' "
300302
+ "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+
+ "PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata "
304+
+ "schema are suppressed from code generation. Only applies when library=spring-boot or spring-declarative-http-interface.",
303305
substituteGenericPagedModel);
304306
addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject);
305307
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
@@ -741,7 +743,7 @@ public void processOpts() {
741743
this.setGeneratePageableConstraintValidation(convertPropertyToBoolean(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION));
742744
}
743745
writePropertyBack(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, generatePageableConstraintValidation);
744-
if (additionalProperties.containsKey(SUBSTITUTE_GENERIC_PAGED_MODEL) && library.equals(SPRING_BOOT)) {
746+
if (additionalProperties.containsKey(SUBSTITUTE_GENERIC_PAGED_MODEL) && (library.equals(SPRING_BOOT) || library.equals(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY))) {
745747
this.setSubstituteGenericPagedModel(convertPropertyToBoolean(SUBSTITUTE_GENERIC_PAGED_MODEL));
746748
}
747749
writePropertyBack(SUBSTITUTE_GENERIC_PAGED_MODEL, substituteGenericPagedModel);
@@ -1142,21 +1144,24 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
11421144
pagedModelRegistry.get(codegenOperation.returnBaseType);
11431145
if (detected != null) {
11441146
String oldType = codegenOperation.returnType;
1145-
String newBaseType = "PagedModel<" + detected.itemSchemaName + ">";
1147+
// Run through toModelName so that schemaMappings (e.g. User → com.example.MyUser)
1148+
// are honored: the mapped name is used both in the type arg and for import resolution.
1149+
String itemType = toModelName(detected.itemSchemaName);
1150+
String newBaseType = pagedModelClassName + "<" + itemType + ">";
11461151
codegenOperation.returnType = newBaseType;
1147-
codegenOperation.returnBaseType = "PagedModel";
1152+
codegenOperation.returnBaseType = pagedModelClassName;
11481153
// Clear any container flag — PagedModel is not itself a List/array
11491154
codegenOperation.returnContainer = null;
1150-
// Add item type import (needed for PagedModel<User> in method signature)
1151-
codegenOperation.imports.add(detected.itemSchemaName);
1152-
codegenOperation.imports.add("PagedModel");
1155+
// Add item type import (needed for PagedModel<T> in method signature)
1156+
codegenOperation.imports.add(itemType);
1157+
codegenOperation.imports.add(pagedModelClassName);
11531158
// Remove paged schema import when no annotations are generated —
11541159
// the class is suppressed and not referenced anywhere
11551160
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
11561161
codegenOperation.imports.remove(detected.schemaName);
11571162
}
1158-
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with PagedModel<{}>",
1159-
codegenOperation.operationId, oldType, detected.itemSchemaName);
1163+
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with {}<{}>",
1164+
codegenOperation.operationId, oldType, pagedModelClassName, itemType);
11601165
}
11611166
}
11621167

@@ -1199,10 +1204,25 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
11991204
}
12001205
}
12011206

1202-
if (SPRING_BOOT.equals(library) && substituteGenericPagedModel) {
1207+
if ((SPRING_BOOT.equals(library) || SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY.equals(library)) && substituteGenericPagedModel) {
12031208
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
12041209
if (!pagedModelRegistry.isEmpty()) {
1205-
importMapping.putIfAbsent("PagedModel", "org.springframework.data.web.PagedModel");
1210+
boolean customMapping = importMapping.containsKey("PagedModel");
1211+
importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel");
1212+
if (!customMapping) {
1213+
// No custom class provided — generate the simple PagedModel into the config package.
1214+
supportingFiles.add(new SupportingFile("pagedModel.mustache",
1215+
(sourceFolder + File.separator + configPackage).replace(".", File.separator), "PagedModel.kt"));
1216+
}
1217+
// Derive the actual simple class name from the FQN in importMapping so that a
1218+
// custom mapping (e.g. "PagedModel" → "com.example.MyPagedModel") is respected.
1219+
// The simple name of the FQN becomes the token used in generated code, and is
1220+
// registered in importMapping so that template import resolution works.
1221+
String fqn = importMapping.get("PagedModel");
1222+
pagedModelClassName = fqn.substring(fqn.lastIndexOf('.') + 1);
1223+
if (!pagedModelClassName.equals("PagedModel")) {
1224+
importMapping.putIfAbsent(pagedModelClassName, fqn);
1225+
}
12061226
LOGGER.info("substituteGenericPagedModel: detected {} paged-model schema(s): {}",
12071227
pagedModelRegistry.size(), pagedModelRegistry.keySet());
12081228
}

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

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ public enum RequestMappingMode {
203203
private Map<String, SpringPageableScanUtils.PageableConstraintsData> pageableConstraintsRegistry = new HashMap<>();
204204
// Map from schema name to detected paged-model info (populated when substituteGenericPagedModel=true)
205205
private Map<String, PagedModelScanUtils.DetectedPagedModel> pagedModelRegistry = new HashMap<>();
206+
// Simple class name of the PagedModel substitute (derived from importMapping; defaults to "PagedModel")
207+
private String pagedModelClassName = "PagedModel";
206208

207209
public SpringCodegen() {
208210
super();
@@ -374,10 +376,10 @@ public SpringCodegen() {
374376
+ "Requires useBeanValidation=true and library=spring-boot.",
375377
generatePageableConstraintValidation));
376378
cliOptions.add(CliOption.newBoolean(SUBSTITUTE_GENERIC_PAGED_MODEL,
377-
"Detect schemas that represent paginated responses (an object with a 'content' array property and a "
379+
"Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' "
378380
+ "pagination-metadata property) and replace their generated references with "
379-
+ "org.springframework.data.web.PagedModel<T>. The detected page schemas and the pagination metadata "
380-
+ "schema are suppressed from code generation. Only applies when library=spring-boot.",
381+
+ "PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata "
382+
+ "schema are suppressed from code generation. Only applies when library=spring-boot or spring-http-interface.",
381383
substituteGenericPagedModel));
382384

383385
}
@@ -588,11 +590,14 @@ public void processOpts() {
588590

589591
convertPropertyToBooleanAndWriteBack(ADDITIONAL_NOT_NULL_ANNOTATIONS, this::setAdditionalNotNullAnnotations);
590592

593+
if (SPRING_BOOT.equals(library) || SPRING_HTTP_INTERFACE.equals(library)) {
594+
convertPropertyToBooleanAndWriteBack(SUBSTITUTE_GENERIC_PAGED_MODEL, this::setSubstituteGenericPagedModel);
595+
}
596+
591597
if (SPRING_BOOT.equals(library)) {
592598
convertPropertyToBooleanAndWriteBack(AUTO_X_SPRING_PAGINATED, this::setAutoXSpringPaginated);
593599
convertPropertyToBooleanAndWriteBack(GENERATE_SORT_VALIDATION, this::setGenerateSortValidation);
594600
convertPropertyToBooleanAndWriteBack(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, this::setGeneratePageableConstraintValidation);
595-
convertPropertyToBooleanAndWriteBack(SUBSTITUTE_GENERIC_PAGED_MODEL, this::setSubstituteGenericPagedModel);
596601
}
597602

598603
// override parent one
@@ -867,10 +872,25 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
867872
}
868873
}
869874

870-
if (SPRING_BOOT.equals(library) && substituteGenericPagedModel) {
875+
if ((SPRING_BOOT.equals(library) || SPRING_HTTP_INTERFACE.equals(library)) && substituteGenericPagedModel) {
871876
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
872877
if (!pagedModelRegistry.isEmpty()) {
873-
importMapping.putIfAbsent("PagedModel", "org.springframework.data.web.PagedModel");
878+
boolean customMapping = importMapping.containsKey("PagedModel");
879+
importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel");
880+
if (!customMapping) {
881+
// No custom class provided — generate the simple PagedModel into the config package.
882+
supportingFiles.add(new SupportingFile("pagedModel.mustache",
883+
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "PagedModel.java"));
884+
}
885+
// Derive the actual simple class name from the FQN in importMapping so that a
886+
// custom mapping (e.g. "PagedModel" → "com.example.MyPagedModel") is respected.
887+
// The simple name of the FQN becomes the token used in generated code, and is
888+
// registered in importMapping so that template import resolution works.
889+
String fqn = importMapping.get("PagedModel");
890+
pagedModelClassName = fqn.substring(fqn.lastIndexOf('.') + 1);
891+
if (!pagedModelClassName.equals("PagedModel")) {
892+
importMapping.put(pagedModelClassName, fqn);
893+
}
874894
LOGGER.info("substituteGenericPagedModel: detected {} paged-model schema(s): {}",
875895
pagedModelRegistry.size(), pagedModelRegistry.keySet());
876896
}
@@ -1352,21 +1372,24 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
13521372
pagedModelRegistry.get(codegenOperation.returnBaseType);
13531373
if (detected != null) {
13541374
String oldType = codegenOperation.returnType;
1355-
String newBaseType = "PagedModel<" + detected.itemSchemaName + ">";
1375+
// Run through toModelName so that schemaMappings (e.g. User → com.example.MyUser)
1376+
// are honored: the mapped name is used both in the type arg and for import resolution.
1377+
String itemType = toModelName(detected.itemSchemaName);
1378+
String newBaseType = pagedModelClassName + "<" + itemType + ">";
13561379
codegenOperation.returnType = newBaseType;
1357-
codegenOperation.returnBaseType = "PagedModel";
1380+
codegenOperation.returnBaseType = pagedModelClassName;
13581381
// Clear any container flag — PagedModel is not itself a List/array
13591382
codegenOperation.returnContainer = null;
1360-
// Add item type import (needed for PagedModel<User> in method signature)
1361-
codegenOperation.imports.add(detected.itemSchemaName);
1362-
codegenOperation.imports.add("PagedModel");
1383+
// Add item type import (needed for PagedModel<T> in method signature)
1384+
codegenOperation.imports.add(itemType);
1385+
codegenOperation.imports.add(pagedModelClassName);
13631386
// Remove paged schema import when no annotations are generated —
13641387
// the class is suppressed and not referenced anywhere
13651388
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
13661389
codegenOperation.imports.remove(detected.schemaName);
13671390
}
1368-
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with PagedModel<{}>",
1369-
codegenOperation.operationId, oldType, detected.itemSchemaName);
1391+
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with {}<{}>",
1392+
codegenOperation.operationId, oldType, pagedModelClassName, itemType);
13701393
}
13711394
}
13721395

0 commit comments

Comments
 (0)