Skip to content

Commit 3c092b4

Browse files
authored
[KOTLIN;SPRING] - add support for 'x-spring-paginated' to get closer to feature parity with java-spring codegen; add 'autoXSpringPaginated' option; support x-operation-extra-annotation (#22958)
* add x-kotlin-implements * implement tests * update samples * fix tests - forbidden api issue * add samples * add samples. use Pageable only for server-side * add support for auto-detecting x-spring-paginated in Spring Boot operations * fix maven dependencies import * add unit tests * add support for vendor extension * remove files * fix samples * fix docs * implement suggestions from CR. Fix declarative interface naming. * move import around * add x-operation-extra-annotation * make sure the PageableAsQueryParam does not remove already present x-operation-extra-annotation content * support also list format * regenerate samples and docs * regenerate samples and docs * force tests rerun * remove files * add files * trigger test rerun
1 parent 45c3692 commit 3c092b4

79 files changed

Lines changed: 2333 additions & 349 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/generators/kotlin-spring.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
2424
|apiSuffix|suffix for api classes| |Api|
2525
|artifactId|Generated artifact id (name of jar).| |openapi-spring|
2626
|artifactVersion|Generated artifact's package version.| |1.0.0|
27+
|autoXSpringPaginated|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.| |false|
2728
|basePackage|base package (invokerPackage) for generated code| |org.openapitools|
2829
|beanQualifiers|Whether to add fully-qualifier class names as bean qualifiers in @Component and @RestController annotations. May be used to prevent bean names clash if multiple generated libraries (contexts) added to single project.| |false|
2930
|configPackage|configuration package for generated code| |org.openapitools.configuration|
@@ -73,12 +74,14 @@ These options may be applied as additional-properties (cli) or configOptions (pl
7374
|x-content-type|Specify custom value for 'Content-Type' header for operation|OPERATION|null
7475
|x-discriminator-value|Used with model inheritance to specify value for discriminator that identifies current model|MODEL|
7576
|x-field-extra-annotation|List of custom annotations to be added to property|FIELD, OPERATION_PARAMETER|null
77+
|x-operation-extra-annotation|List of custom annotations to be added to operation|OPERATION|null
7678
|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
7779
|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
7880
|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
7981
|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
8082
|x-kotlin-implements|Ability to specify interfaces that model must implement|MODEL|empty array
8183
|x-kotlin-implements-fields|Specify attributes that are implemented by the interface(s) added via `x-kotlin-implements`|MODEL|empty array
84+
|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
8285

8386

8487
## IMPORT MAPPING

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

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import java.net.URL;
4646
import java.util.*;
4747
import java.util.regex.Matcher;
48+
import java.util.stream.Collectors;
4849

4950
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
5051
import static org.openapitools.codegen.utils.StringUtils.camelize;
@@ -95,6 +96,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
9596
public static final String REQUEST_MAPPING_OPTION = "requestMappingMode";
9697
public static final String USE_REQUEST_MAPPING_ON_CONTROLLER = "useRequestMappingOnController";
9798
public static final String USE_REQUEST_MAPPING_ON_INTERFACE = "useRequestMappingOnInterface";
99+
public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated";
98100

99101
@Getter
100102
public enum DeclarativeInterfaceReactiveMode {
@@ -158,6 +160,7 @@ public String getDescription() {
158160
@Setter private boolean beanQualifiers = false;
159161
@Setter private DeclarativeInterfaceReactiveMode declarativeInterfaceReactiveMode = DeclarativeInterfaceReactiveMode.coroutines;
160162
@Setter private boolean useResponseEntity = true;
163+
@Setter private boolean autoXSpringPaginated = false;
161164

162165
@Getter @Setter
163166
protected boolean useSpringBoot3 = false;
@@ -251,6 +254,7 @@ public KotlinSpringServerCodegen() {
251254
addOption(X_KOTLIN_IMPLEMENTS_FIELDS_SKIP, "A list of fields per schema name that should NOT be created with `override` keyword despite their presence in vendor extension `x-kotlin-implements-fields` for the schema. Example: yaml `xKotlinImplementsFieldsSkip: Pet: [photoUrls]` skips `override` for `photoUrls` in schema `Pet`", "empty map");
252255
addOption(SCHEMA_IMPLEMENTS, "A map of single interface or a list of interfaces per schema name that should be implemented (serves similar purpose as `x-kotlin-implements`, but is fully decoupled from the api spec). Example: yaml `schemaImplements: {Pet: com.some.pack.WithId, Category: [com.some.pack.CategoryInterface], Dog: [com.some.pack.Canine, com.some.pack.OtherInterface]}` implements interfaces in schemas `Pet` (interface `com.some.pack.WithId`), `Category` (interface `com.some.pack.CategoryInterface`), `Dog`(interfaces `com.some.pack.Canine`, `com.some.pack.OtherInterface`)", "empty map");
253256
addOption(SCHEMA_IMPLEMENTS_FIELDS, "A map of single field or a list of fields per schema name that should be prepended with `override` (serves similar purpose as `x-kotlin-implements-fields`, but is fully decoupled from the api spec). Example: yaml `schemaImplementsFields: {Pet: id, Category: [name, id], Dog: [bark, breed]}` marks fields to be prepended with `override` in schemas `Pet` (field `id`), `Category` (fields `name`, `id`) and `Dog` (fields `bark`, `breed`)", "empty map");
257+
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);
254258
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
255259
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
256260
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
@@ -447,6 +451,12 @@ public void processOpts() {
447451
additionalProperties.put(DOCUMENTATION_PROVIDER, DocumentationProvider.NONE);
448452
additionalProperties.put(ANNOTATION_LIBRARY, AnnotationLibrary.NONE);
449453
}
454+
if (additionalProperties.containsKey(USE_SPRING_BOOT3)) {
455+
this.setUseSpringBoot3(convertPropertyToBoolean(USE_SPRING_BOOT3));
456+
}
457+
if (additionalProperties.containsKey(INCLUDE_HTTP_REQUEST_CONTEXT)) {
458+
this.setIncludeHttpRequestContext(convertPropertyToBoolean(INCLUDE_HTTP_REQUEST_CONTEXT));
459+
}
450460

451461
if (isModelMutable()) {
452462
typeMapping.put("array", "kotlin.collections.MutableList");
@@ -470,6 +480,14 @@ public void processOpts() {
470480
// used later in recursive import in postProcessingModels
471481
importMapping.put("com.fasterxml.jackson.annotation.JsonProperty", "com.fasterxml.jackson.annotation.JsonCreator");
472482

483+
// Spring-specific import mappings for x-spring-paginated support
484+
importMapping.put("ApiIgnore", "springfox.documentation.annotations.ApiIgnore");
485+
importMapping.put("ParameterObject", "org.springdoc.api.annotations.ParameterObject");
486+
importMapping.put("PageableAsQueryParam", "org.springdoc.core.converters.models.PageableAsQueryParam");
487+
if (useSpringBoot3) {
488+
importMapping.put("ParameterObject", "org.springdoc.core.annotations.ParameterObject");
489+
}
490+
473491
if (!additionalProperties.containsKey(CodegenConstants.LIBRARY)) {
474492
additionalProperties.put(CodegenConstants.LIBRARY, library);
475493
}
@@ -642,13 +660,10 @@ public void processOpts() {
642660
if (additionalProperties.containsKey(USE_TAGS)) {
643661
this.setUseTags(Boolean.parseBoolean(additionalProperties.get(USE_TAGS).toString()));
644662
}
645-
646-
if (additionalProperties.containsKey(USE_SPRING_BOOT3)) {
647-
this.setUseSpringBoot3(convertPropertyToBoolean(USE_SPRING_BOOT3));
648-
}
649-
if (additionalProperties.containsKey(INCLUDE_HTTP_REQUEST_CONTEXT)) {
650-
this.setIncludeHttpRequestContext(convertPropertyToBoolean(INCLUDE_HTTP_REQUEST_CONTEXT));
663+
if (additionalProperties.containsKey(AUTO_X_SPRING_PAGINATED) && library.equals(SPRING_BOOT)) {
664+
this.setAutoXSpringPaginated(convertPropertyToBoolean(AUTO_X_SPRING_PAGINATED));
651665
}
666+
writePropertyBack(AUTO_X_SPRING_PAGINATED, autoXSpringPaginated);
652667
if (isUseSpringBoot3()) {
653668
if (DocumentationProvider.SPRINGFOX.equals(getDocumentationProvider())) {
654669
throw new IllegalArgumentException(DocumentationProvider.SPRINGFOX.getPropertyName() + " is not supported with Spring Boot > 3.x");
@@ -802,7 +817,7 @@ public void processOpts() {
802817
gradleWrapperPackage.replace(".", File.separator), "gradle-wrapper.jar"));
803818
}
804819

805-
apiTemplateFiles.put("apiInterface.mustache", "Client.kt");
820+
apiTemplateFiles.put("apiInterface.mustache", ".kt");
806821
apiTestTemplateFiles.clear();
807822
}
808823

@@ -872,6 +887,108 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera
872887
}
873888
}
874889

890+
/**
891+
* Processes operations to support the x-spring-paginated vendor extension.
892+
*
893+
* When x-spring-paginated is set to true on an operation, this method:
894+
* - Adds org.springframework.data.domain.Pageable parameter to the method signature
895+
* - Removes the default Spring Data Web pagination query parameters (page, size, sort)
896+
* - Adds appropriate imports (Pageable, ApiIgnore for springfox, ParameterObject for springdoc)
897+
*
898+
* Auto-detection (when autoXSpringPaginated is enabled):
899+
* - Automatically detects operations with 'page', 'size', and 'sort' query parameters (case-sensitive)
900+
* - Applies x-spring-paginated behavior to these operations automatically
901+
* - Respects manual x-spring-paginated: false setting (manual override takes precedence)
902+
* - Only applies when library is spring-boot
903+
*
904+
* Note: x-spring-paginated is ONLY applied for server-side libraries (spring-boot).
905+
* Client libraries (spring-cloud, spring-declarative-http-interface) need actual query parameters
906+
* to send over HTTP, so the extension is ignored for them.
907+
*
908+
* Parameter ordering in generated methods:
909+
* 1. Regular OpenAPI parameters (allParams)
910+
* 2. Optional HttpServletRequest/ServerWebExchange (if includeHttpRequestContext is enabled)
911+
* 3. Pageable parameter (if x-spring-paginated is true and library is spring-boot)
912+
*
913+
* This implementation mirrors the behavior in SpringCodegen for consistency.
914+
*
915+
* @param path the operation path
916+
* @param httpMethod the HTTP method
917+
* @param operation the OpenAPI operation
918+
* @param servers the list of servers
919+
* @return the processed CodegenOperation
920+
*/
921+
@Override
922+
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<io.swagger.v3.oas.models.servers.Server> servers) {
923+
// #8315 Spring Data Web default query params recognized by Pageable
924+
List<String> defaultPageableQueryParams = Arrays.asList("page", "size", "sort");
925+
926+
CodegenOperation codegenOperation = super.fromOperation(path, httpMethod, operation, servers);
927+
928+
// Check if operation has all three pagination query parameters (case-sensitive)
929+
boolean hasParamsForPageable = codegenOperation.queryParams.stream()
930+
.map(p -> p.baseName)
931+
.collect(Collectors.toSet())
932+
.containsAll(defaultPageableQueryParams);
933+
// Auto-detect pagination parameters and add x-spring-paginated if autoXSpringPaginated is enabled
934+
// Only for spring-boot library, respect manual x-spring-paginated: false setting
935+
if (SPRING_BOOT.equals(library) && autoXSpringPaginated) {
936+
// Check if x-spring-paginated is not explicitly set to false
937+
if (operation.getExtensions() == null || !Boolean.FALSE.equals(operation.getExtensions().get("x-spring-paginated"))) {
938+
939+
940+
if (hasParamsForPageable) {
941+
// Automatically add x-spring-paginated to the operation
942+
if (operation.getExtensions() == null) {
943+
operation.setExtensions(new HashMap<>());
944+
}
945+
operation.getExtensions().put("x-spring-paginated", Boolean.TRUE);
946+
codegenOperation.vendorExtensions.put("x-spring-paginated", Boolean.TRUE);
947+
}
948+
}
949+
}
950+
951+
// Only process x-spring-paginated for server-side libraries (spring-boot)
952+
// Client libraries (spring-cloud, spring-declarative-http-interface) need actual query parameters for HTTP requests
953+
if (SPRING_BOOT.equals(library)) {
954+
// add Pageable import only if x-spring-paginated explicitly used AND it's a server library
955+
// this allows to use a custom Pageable schema without importing Spring Pageable.
956+
if (operation.getExtensions() != null && Boolean.TRUE.equals(operation.getExtensions().get("x-spring-paginated"))) {
957+
importMapping.putIfAbsent("Pageable", "org.springframework.data.domain.Pageable");
958+
}
959+
960+
// add org.springframework.data.domain.Pageable import when needed (server libraries only)
961+
if (operation.getExtensions() != null && Boolean.TRUE.equals(operation.getExtensions().get("x-spring-paginated"))) {
962+
codegenOperation.imports.add("Pageable");
963+
if (DocumentationProvider.SPRINGFOX.equals(getDocumentationProvider())) {
964+
codegenOperation.imports.add("ApiIgnore");
965+
}
966+
if (DocumentationProvider.SPRINGDOC.equals(getDocumentationProvider())) {
967+
codegenOperation.imports.add("PageableAsQueryParam");
968+
// Prepend @PageableAsQueryParam to existing x-operation-extra-annotation if present
969+
// Use getObjectAsStringList to properly handle both list and string formats:
970+
// - YAML list: ['@Ann1', '@Ann2'] -> List of annotations
971+
// - Single string: '@Ann1 @Ann2' -> Single-element list
972+
// - Nothing/null -> Empty list
973+
Object existingAnnotation = codegenOperation.vendorExtensions.get("x-operation-extra-annotation");
974+
List<String> annotations = DefaultCodegen.getObjectAsStringList(existingAnnotation);
975+
976+
// Prepend @PageableAsQueryParam to the beginning of the list
977+
List<String> updatedAnnotations = new ArrayList<>();
978+
updatedAnnotations.add("@PageableAsQueryParam");
979+
updatedAnnotations.addAll(annotations);
980+
981+
codegenOperation.vendorExtensions.put("x-operation-extra-annotation", updatedAnnotations);
982+
}
983+
984+
// #8315 Remove matching Spring Data Web default query params if 'x-spring-paginated' with Pageable is used
985+
codegenOperation.queryParams.removeIf(param -> defaultPageableQueryParams.contains(param.baseName));
986+
codegenOperation.allParams.removeIf(param -> param.isQueryParam && defaultPageableQueryParams.contains(param.baseName));
987+
}
988+
}
989+
return codegenOperation;
990+
}
991+
875992
@Override
876993
public void preprocessOpenAPI(OpenAPI openAPI) {
877994
super.preprocessOpenAPI(openAPI);
@@ -1117,12 +1234,14 @@ public List<VendorExtension> getSupportedVendorExtensions() {
11171234
extensions.add(VendorExtension.X_CONTENT_TYPE);
11181235
extensions.add(VendorExtension.X_DISCRIMINATOR_VALUE);
11191236
extensions.add(VendorExtension.X_FIELD_EXTRA_ANNOTATION);
1237+
extensions.add(VendorExtension.X_OPERATION_EXTRA_ANNOTATION);
11201238
extensions.add(VendorExtension.X_PATTERN_MESSAGE);
11211239
extensions.add(VendorExtension.X_SIZE_MESSAGE);
11221240
extensions.add(VendorExtension.X_MINIMUM_MESSAGE);
11231241
extensions.add(VendorExtension.X_MAXIMUM_MESSAGE);
11241242
extensions.add(VendorExtension.X_KOTLIN_IMPLEMENTS);
11251243
extensions.add(VendorExtension.X_KOTLIN_IMPLEMENTS_FIELDS);
1244+
extensions.add(VendorExtension.X_SPRING_PAGINATED);
11261245
return extensions;
11271246
}
11281247

modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v
8484
authorizations = [{{#authMethods}}Authorization(value = "{{name}}"{{#isOAuth}}, scopes = [{{#scopes}}AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}}, {{/-last}}{{/scopes}}]{{/isOAuth}}){{^-last}}, {{/-last}}{{/authMethods}}]{{/hasAuthMethods}})
8585
@ApiResponses(
8686
value = [{{#responses}}ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}::class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}{{/responses}}]){{/swagger1AnnotationLibrary}}
87+
{{#vendorExtensions.x-operation-extra-annotation}}
88+
{{{.}}}
89+
{{/vendorExtensions.x-operation-extra-annotation}}
8790
@RequestMapping(
8891
method = [RequestMethod.{{httpMethod}}],
8992
// "{{#lambdaEscapeInNormalString}}{{{path}}}{{/lambdaEscapeInNormalString}}"
@@ -95,7 +98,9 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v
9598
)
9699
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}
97100
{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}},
98-
{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#hasParams}}
101+
{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
102+
{{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},
103+
{{/includeHttpRequestContext}}{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}}
99104
{{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}} {
100105
return {{>returnValue}}
101106
}

0 commit comments

Comments
 (0)