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..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
@@ -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,9 @@ 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 +818,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 +896,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 +970,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 +1018,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 +1157,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 +1246,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>");
+ }
}