From 7439ae672729d716c567f7c96db24396065cf6c6 Mon Sep 17 00:00:00 2001 From: Narbe66 Date: Sun, 23 Nov 2025 10:44:21 +0100 Subject: [PATCH 1/2] Add support for paginated return types in Spring code generation --- docs/generators/spring.md | 37 +++--- .../codegen/languages/SpringCodegen.java | 116 ++++++++++++++---- .../java/spring/SpringCodegenTest.java | 24 ++++ 3 files changed, 137 insertions(+), 40 deletions(-) diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 2af0fe826a3e..4dc798bb3376 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -68,6 +68,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |modelPackage|package for generated models| |org.openapitools.model| |openApiNullable|Enable OpenAPI Jackson Nullable library. Not supported by `microprofile` library.| |true| |optionalAcceptNullable|Use `ofNullable` instead of just `of` to accept null values when using Optional.| |true| +|paginatedReturnType|Set container type to use for collection responses when the operation has vendor extension `x-spring-paginated`. Applies only to collection responses.|
**LIST**
Keep using `java.util.List` for collection responses (default).
**SLICE**
Use `org.springframework.data.domain.Slice` for collection responses when `x-spring-paginated` is enabled.
**PAGE**
Use `org.springframework.data.domain.Page` for collection responses when `x-spring-paginated` is enabled.
|LIST| |parentArtifactId|parent artifactId in generated pom N.B. parentGroupId, parentArtifactId and parentVersion must all be specified for any of them to take effect| |null| |parentGroupId|parent groupId in generated pom N.B. parentGroupId, parentArtifactId and parentVersion must all be specified for any of them to take effect| |null| |parentVersion|parent version in generated pom N.B. parentGroupId, parentArtifactId and parentVersion must all be specified for any of them to take effect| |null| @@ -112,24 +113,24 @@ These options may be applied as additional-properties (cli) or configOptions (pl ## SUPPORTED VENDOR EXTENSIONS -| Extension name | Description | Applicable for | Default value | -| -------------- | ----------- | -------------- | ------------- | -|x-discriminator-value|Used with model inheritance to specify value for discriminator that identifies current model|MODEL| -|x-implements|Ability to specify interfaces that model must implements|MODEL|empty array -|x-setter-extra-annotation|Custom annotation that can be specified over java setter for specific field|FIELD|When field is array & uniqueItems, then this extension is used to add `@JsonDeserialize(as = LinkedHashSet.class)` over setter, otherwise no value -|x-tags|Specify multiple swagger tags for operation|OPERATION|null -|x-accepts|Specify custom value for 'Accept' header for operation|OPERATION|null -|x-content-type|Specify custom value for 'Content-Type' header for operation|OPERATION|null -|x-class-extra-annotation|List of custom annotations to be added to model|MODEL|null -|x-field-extra-annotation|List of custom annotations to be added to property|FIELD, OPERATION_PARAMETER|null -|x-operation-extra-annotation|List of custom annotations to be added to operation|OPERATION|null -|x-spring-paginated|Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object.|OPERATION|false -|x-version-param|Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false|OPERATION_PARAMETER|null -|x-pattern-message|Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable|FIELD, OPERATION_PARAMETER|null -|x-size-message|Add this property whenever you need to customize the invalidation error message for the size or length of a variable|FIELD, OPERATION_PARAMETER|null -|x-minimum-message|Add this property whenever you need to customize the invalidation error message for the minimum value of a variable|FIELD, OPERATION_PARAMETER|null -|x-maximum-message|Add this property whenever you need to customize the invalidation error message for the maximum value of a variable|FIELD, OPERATION_PARAMETER|null -|x-spring-api-version|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).|OPERATION|null +| Extension name | Description | Applicable for | Default value | +| -------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -------------- | ------------- | +|x-discriminator-value| Used with model inheritance to specify value for discriminator that identifies current model |MODEL| +|x-implements| Ability to specify interfaces that model must implements |MODEL|empty array +|x-setter-extra-annotation| Custom annotation that can be specified over java setter for specific field |FIELD|When field is array & uniqueItems, then this extension is used to add `@JsonDeserialize(as = LinkedHashSet.class)` over setter, otherwise no value +|x-tags| Specify multiple swagger tags for operation |OPERATION|null +|x-accepts| Specify custom value for 'Accept' header for operation |OPERATION|null +|x-content-type| Specify custom value for 'Content-Type' header for operation |OPERATION|null +|x-class-extra-annotation| List of custom annotations to be added to model |MODEL|null +|x-field-extra-annotation| List of custom annotations to be added to property |FIELD, OPERATION_PARAMETER|null +|x-operation-extra-annotation| List of custom annotations to be added to operation |OPERATION|null +|x-spring-paginated| Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object. Can be modified with paginatedReturnType configOption |OPERATION|false +|x-version-param| Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false |OPERATION_PARAMETER|null +|x-pattern-message| Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable |FIELD, OPERATION_PARAMETER|null +|x-size-message| Add this property whenever you need to customize the invalidation error message for the size or length of a variable |FIELD, OPERATION_PARAMETER|null +|x-minimum-message| Add this property whenever you need to customize the invalidation error message for the minimum value of a variable |FIELD, OPERATION_PARAMETER|null +|x-maximum-message| Add this property whenever you need to customize the invalidation error message for the maximum value of a variable |FIELD, OPERATION_PARAMETER|null +|x-spring-api-version| Value for 'version' attribute in @RequestMapping (for Spring 7 and above). |OPERATION|null ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index a2cc1bbc190d..4df270fea2af 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -58,6 +58,7 @@ public class SpringCodegen extends AbstractJavaCodegen implements BeanValidationFeatures, PerformBeanValidationFeatures, OptionalFeatures, SwaggerUIFeatures { + private static final String PAGINATED_RETURN_OPTION = "paginatedReturnType"; private final Logger LOGGER = LoggerFactory.getLogger(SpringCodegen.class); public static final String TITLE = "title"; public static final String SERVER_PORT = "serverPort"; @@ -108,13 +109,26 @@ public enum RequestMappingMode { controller("Generate the @RequestMapping annotation on the generated Api Controller Implementation."), none("Do not add a class level @RequestMapping annotation."); - private String description; + private final String description; RequestMappingMode(String description) { this.description = description; } } + @Getter + public enum PaginatedReturnType { + LIST("Keep using java.util.List for collection responses (default)."), + SLICE("Use org.springframework.data.domain.Slice for collection responses when x-spring-paginated is enabled."), + PAGE("Use org.springframework.data.domain.Page for collection responses when x-spring-paginated is enabled."); + + private final String description; + + PaginatedReturnType(String description) { + this.description = description; + } + } + public static final String OPEN_BRACE = "{"; public static final String CLOSE_BRACE = "}"; @@ -158,6 +172,8 @@ public enum RequestMappingMode { @Getter @Setter protected RequestMappingMode requestMappingMode = RequestMappingMode.controller; @Getter @Setter + protected PaginatedReturnType paginatedReturnType = PaginatedReturnType.LIST; + @Getter @Setter protected boolean optionalAcceptNullable = true; @Getter @Setter protected boolean useSpringBuiltInValidation = false; @@ -258,6 +274,14 @@ public SpringCodegen() { } cliOptions.add(requestMappingOpt); + CliOption paginatedReturnOpt = new CliOption(PAGINATED_RETURN_OPTION, + "Set container type to use for collection responses when the operation has vendor extension 'x-spring-paginated'. Applies only to collection responses.") + .defaultValue(paginatedReturnType.name()); + for (PaginatedReturnType v : PaginatedReturnType.values()) { + paginatedReturnOpt.addEnum(v.name(), v.getDescription()); + } + cliOptions.add(paginatedReturnOpt); + cliOptions.add(CliOption.newBoolean(UNHANDLED_EXCEPTION_HANDLING, "Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).", unhandledException)); @@ -376,6 +400,7 @@ public void processOpts() { } convertPropertyToTypeAndWriteBack(REQUEST_MAPPING_OPTION, RequestMappingMode::valueOf, this::setRequestMappingMode); + convertPropertyToTypeAndWriteBack(PAGINATED_RETURN_OPTION, PaginatedReturnType::valueOf, this::setPaginatedReturnType); // Please refrain from updating values of Config Options after super.ProcessOpts() is called super.processOpts(); @@ -732,9 +757,10 @@ public void preprocessOpenAPI(OpenAPI openAPI) { value.put("tag", tag); tags.add(value); } - if (operation.getTags().size() > 0) { + if (!operation.getTags() + .isEmpty()) { final String tag = operation.getTags().get(0); - operation.setTags(Arrays.asList(tag)); + operation.setTags(Collections.singletonList(tag)); } operation.addExtension("x-tags", tags); } @@ -793,6 +819,53 @@ public void setIsVoid(boolean isVoid) { } }); + // If the operation has x-spring-paginated and the return is a collection (List), + // adjust the container type according to the configured paginatedReturnType. + if (operation.vendorExtensions.containsKey("x-spring-paginated")) { + if ("List".equals(operation.returnContainer)) { + switch (paginatedReturnType) { + case PAGE: + operation.returnContainer = "Page"; + break; + case SLICE: + operation.returnContainer = "Slice"; + break; + case LIST: + default: + // keep List + } + if ("Page".equals(operation.returnContainer) || "Slice".equals(operation.returnContainer)) { + // ensure import and importMapping for Page/Slice + operation.imports.add(operation.returnContainer); + importMapping.put(operation.returnContainer, "org.springframework.data.domain." + operation.returnContainer); + } + } + + // Also apply to responses that are collections + if (operation.responses != null) { + for (final CodegenResponse resp : operation.responses) { + if ("List".equals(resp.containerType)) { + switch (paginatedReturnType) { + case PAGE: + resp.containerType = "Page"; + break; + case SLICE: + resp.containerType = "Slice"; + break; + case LIST: + default: + // keep List + } + if ("Page".equals(resp.containerType) || "Slice".equals(resp.containerType)) { + // imports are kept on operation level + operation.imports.add(resp.containerType); + importMapping.put(resp.containerType, "org.springframework.data.domain." + resp.containerType); + } + } + } + } + } + prepareVersioningParameters(ops); handleImplicitHeaders(operation); } @@ -824,29 +897,28 @@ private interface DataTypeAssigner { * fields in the model. */ private void doDataTypeAssignment(String returnType, DataTypeAssigner dataTypeAssigner) { - final String rt = returnType; - if (rt == null) { + if (returnType == null) { dataTypeAssigner.setReturnType("Void"); dataTypeAssigner.setIsVoid(true); - } else if (rt.startsWith("List") || rt.startsWith("java.util.List")) { - final int start = rt.indexOf("<"); - final int end = rt.lastIndexOf(">"); + } else if (returnType.startsWith("List") || returnType.startsWith("java.util.List")) { + final int start = returnType.indexOf("<"); + final int end = returnType.lastIndexOf(">"); if (start > 0 && end > 0) { - dataTypeAssigner.setReturnType(rt.substring(start + 1, end).trim()); + dataTypeAssigner.setReturnType(returnType.substring(start + 1, end).trim()); dataTypeAssigner.setReturnContainer("List"); } - } else if (rt.startsWith("Map") || rt.startsWith("java.util.Map")) { - final int start = rt.indexOf("<"); - final int end = rt.lastIndexOf(">"); + } else if (returnType.startsWith("Map") || returnType.startsWith("java.util.Map")) { + final int start = returnType.indexOf("<"); + final int end = returnType.lastIndexOf(">"); if (start > 0 && end > 0) { - dataTypeAssigner.setReturnType(rt.substring(start + 1, end).split(",", 2)[1].trim()); + dataTypeAssigner.setReturnType(returnType.substring(start + 1, end).split(",", 2)[1].trim()); dataTypeAssigner.setReturnContainer("Map"); } - } else if (rt.startsWith("Set") || rt.startsWith("java.util.Set")) { - final int start = rt.indexOf("<"); - final int end = rt.lastIndexOf(">"); + } else if (returnType.startsWith("Set") || returnType.startsWith("java.util.Set")) { + final int start = returnType.indexOf("<"); + final int end = returnType.lastIndexOf(">"); if (start > 0 && end > 0) { - dataTypeAssigner.setReturnType(rt.substring(start + 1, end).trim()); + dataTypeAssigner.setReturnType(returnType.substring(start + 1, end).trim()); dataTypeAssigner.setReturnContainer("Set"); } } @@ -899,7 +971,7 @@ public Map postProcessSupportingFileData(Map obj @Override public String toApiName(String name) { - if (name.length() == 0) { + if (name.isEmpty()) { return "DefaultApi"; } name = sanitizeName(name); @@ -947,10 +1019,10 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert } // Add imports for Jackson - if (!Boolean.TRUE.equals(model.isEnum)) { + if (!model.isEnum) { model.imports.add("JsonProperty"); - if (Boolean.TRUE.equals(model.hasEnums)) { + if (model.hasEnums) { model.imports.add("JsonValue"); } } else { // enum class @@ -1086,7 +1158,7 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation if (schemaTypes.containsKey("array")) { // we have a match with SSE pattern // double check potential conflicting, multiple specs - if (schemaTypes.keySet().size() > 1) { + if (schemaTypes.size() > 1) { throw new RuntimeException("only 1 response media type supported, when SSE is detected"); } // double check schema format @@ -1175,7 +1247,7 @@ public ModelsMap postProcessModelsEnum(ModelsMap objs) { for (CodegenProperty var : cm.vars) { addNullableImports = isAddNullableImports(cm, addNullableImports, var); } - if (Boolean.TRUE.equals(cm.isEnum) && cm.allowableValues != null) { + if (cm.isEnum && cm.allowableValues != null) { cm.imports.add(importMapping.get("JsonValue")); final Map item = new HashMap<>(); item.put("import", importMapping.get("JsonValue")); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 32887949dff2..d76f76c953c0 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -6174,4 +6174,28 @@ public void annotationLibraryDoesNotCauseImportConflictsInSpringWithAnnotationLi "import io.swagger.v3.oas.annotations.media.Schema;" ); } + + @Test + public void testPaginatedReturnTypePage() throws IOException { + SpringCodegen codegen = new SpringCodegen(); + codegen.setLibrary(SPRING_BOOT); + codegen.setPaginatedReturnType(PaginatedReturnType.PAGE); + + Map files = generateFiles(codegen, "src/test/resources/3_0/spring/petstore-with-spring-pageable.yaml"); + + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsByStatus").hasReturnType("ResponseEntity>"); + } + + @Test + public void testPaginatedReturnTypeSlice() throws IOException { + SpringCodegen codegen = new SpringCodegen(); + codegen.setLibrary(SPRING_BOOT); + codegen.setPaginatedReturnType(PaginatedReturnType.SLICE); + + Map files = generateFiles(codegen, "src/test/resources/3_0/spring/petstore-with-spring-pageable.yaml"); + + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsByStatus").hasReturnType("ResponseEntity>"); + } } From 18d95214371d799c9e9cd8e69ce66187a3a023fe Mon Sep 17 00:00:00 2001 From: Narbe66 <47669187+Narbe66@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:36:16 +0100 Subject: [PATCH 2/2] Update modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/org/openapitools/codegen/languages/SpringCodegen.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 4df270fea2af..cee34269ad61 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -757,8 +757,7 @@ public void preprocessOpenAPI(OpenAPI openAPI) { value.put("tag", tag); tags.add(value); } - if (!operation.getTags() - .isEmpty()) { + if (!operation.getTags().isEmpty()) { final String tag = operation.getTags().get(0); operation.setTags(Collections.singletonList(tag)); }