Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|apiSuffix|suffix for api classes| |Api|
|artifactId|Generated artifact id (name of jar).| |openapi-spring|
|artifactVersion|Generated artifact's package version.| |1.0.0|
|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|
|basePackage|base package (invokerPackage) for generated code| |org.openapitools|
|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|
|configPackage|configuration package for generated code| |org.openapitools.configuration|
Expand Down Expand Up @@ -73,12 +74,14 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|x-content-type|Specify custom value for 'Content-Type' header for operation|OPERATION|null
|x-discriminator-value|Used with model inheritance to specify value for discriminator that identifies current model|MODEL|
|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-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-kotlin-implements|Ability to specify interfaces that model must implement|MODEL|empty array
|x-kotlin-implements-fields|Specify attributes that are implemented by the interface(s) added via `x-kotlin-implements`|MODEL|empty array
|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


## IMPORT MAPPING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.stream.Collectors;

import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
import static org.openapitools.codegen.utils.StringUtils.camelize;
Expand Down Expand Up @@ -95,6 +96,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
public static final String REQUEST_MAPPING_OPTION = "requestMappingMode";
public static final String USE_REQUEST_MAPPING_ON_CONTROLLER = "useRequestMappingOnController";
public static final String USE_REQUEST_MAPPING_ON_INTERFACE = "useRequestMappingOnInterface";
public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated";

@Getter
public enum DeclarativeInterfaceReactiveMode {
Expand Down Expand Up @@ -158,6 +160,7 @@ public String getDescription() {
@Setter private boolean beanQualifiers = false;
@Setter private DeclarativeInterfaceReactiveMode declarativeInterfaceReactiveMode = DeclarativeInterfaceReactiveMode.coroutines;
@Setter private boolean useResponseEntity = true;
@Setter private boolean autoXSpringPaginated = false;

@Getter @Setter
protected boolean useSpringBoot3 = false;
Expand Down Expand Up @@ -251,6 +254,7 @@ public KotlinSpringServerCodegen() {
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");
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");
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");
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);
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
Expand Down Expand Up @@ -447,6 +451,12 @@ public void processOpts() {
additionalProperties.put(DOCUMENTATION_PROVIDER, DocumentationProvider.NONE);
additionalProperties.put(ANNOTATION_LIBRARY, AnnotationLibrary.NONE);
}
if (additionalProperties.containsKey(USE_SPRING_BOOT3)) {
this.setUseSpringBoot3(convertPropertyToBoolean(USE_SPRING_BOOT3));
}
if (additionalProperties.containsKey(INCLUDE_HTTP_REQUEST_CONTEXT)) {
this.setIncludeHttpRequestContext(convertPropertyToBoolean(INCLUDE_HTTP_REQUEST_CONTEXT));
}

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

// Spring-specific import mappings for x-spring-paginated support
importMapping.put("ApiIgnore", "springfox.documentation.annotations.ApiIgnore");
importMapping.put("ParameterObject", "org.springdoc.api.annotations.ParameterObject");
importMapping.put("PageableAsQueryParam", "org.springdoc.core.converters.models.PageableAsQueryParam");
if (useSpringBoot3) {
importMapping.put("ParameterObject", "org.springdoc.core.annotations.ParameterObject");
}

if (!additionalProperties.containsKey(CodegenConstants.LIBRARY)) {
additionalProperties.put(CodegenConstants.LIBRARY, library);
}
Expand Down Expand Up @@ -642,13 +660,10 @@ public void processOpts() {
if (additionalProperties.containsKey(USE_TAGS)) {
this.setUseTags(Boolean.parseBoolean(additionalProperties.get(USE_TAGS).toString()));
}

if (additionalProperties.containsKey(USE_SPRING_BOOT3)) {
this.setUseSpringBoot3(convertPropertyToBoolean(USE_SPRING_BOOT3));
}
if (additionalProperties.containsKey(INCLUDE_HTTP_REQUEST_CONTEXT)) {
this.setIncludeHttpRequestContext(convertPropertyToBoolean(INCLUDE_HTTP_REQUEST_CONTEXT));
if (additionalProperties.containsKey(AUTO_X_SPRING_PAGINATED) && library.equals(SPRING_BOOT)) {
this.setAutoXSpringPaginated(convertPropertyToBoolean(AUTO_X_SPRING_PAGINATED));
}
writePropertyBack(AUTO_X_SPRING_PAGINATED, autoXSpringPaginated);
if (isUseSpringBoot3()) {
if (DocumentationProvider.SPRINGFOX.equals(getDocumentationProvider())) {
throw new IllegalArgumentException(DocumentationProvider.SPRINGFOX.getPropertyName() + " is not supported with Spring Boot > 3.x");
Expand Down Expand Up @@ -802,7 +817,7 @@ public void processOpts() {
gradleWrapperPackage.replace(".", File.separator), "gradle-wrapper.jar"));
}

apiTemplateFiles.put("apiInterface.mustache", "Client.kt");
apiTemplateFiles.put("apiInterface.mustache", ".kt");
apiTestTemplateFiles.clear();
}

Expand Down Expand Up @@ -872,6 +887,108 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera
}
}

/**
* Processes operations to support the x-spring-paginated vendor extension.
*
* When x-spring-paginated is set to true on an operation, this method:
* - Adds org.springframework.data.domain.Pageable parameter to the method signature
* - Removes the default Spring Data Web pagination query parameters (page, size, sort)
* - Adds appropriate imports (Pageable, ApiIgnore for springfox, ParameterObject for springdoc)
*
* Auto-detection (when autoXSpringPaginated is enabled):
* - Automatically detects operations with 'page', 'size', and 'sort' query parameters (case-sensitive)
* - Applies x-spring-paginated behavior to these operations automatically
* - Respects manual x-spring-paginated: false setting (manual override takes precedence)
* - Only applies when library is spring-boot
*
* Note: x-spring-paginated is ONLY applied for server-side libraries (spring-boot).
* Client libraries (spring-cloud, spring-declarative-http-interface) need actual query parameters
* to send over HTTP, so the extension is ignored for them.
*
* Parameter ordering in generated methods:
* 1. Regular OpenAPI parameters (allParams)
* 2. Optional HttpServletRequest/ServerWebExchange (if includeHttpRequestContext is enabled)
* 3. Pageable parameter (if x-spring-paginated is true and library is spring-boot)
*
* This implementation mirrors the behavior in SpringCodegen for consistency.
*
* @param path the operation path
* @param httpMethod the HTTP method
* @param operation the OpenAPI operation
* @param servers the list of servers
* @return the processed CodegenOperation
*/
@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<io.swagger.v3.oas.models.servers.Server> servers) {
// #8315 Spring Data Web default query params recognized by Pageable
List<String> defaultPageableQueryParams = Arrays.asList("page", "size", "sort");

CodegenOperation codegenOperation = super.fromOperation(path, httpMethod, operation, servers);

// Check if operation has all three pagination query parameters (case-sensitive)
boolean hasParamsForPageable = codegenOperation.queryParams.stream()
.map(p -> p.baseName)
.collect(Collectors.toSet())
.containsAll(defaultPageableQueryParams);
// Auto-detect pagination parameters and add x-spring-paginated if autoXSpringPaginated is enabled
// Only for spring-boot library, respect manual x-spring-paginated: false setting
if (SPRING_BOOT.equals(library) && autoXSpringPaginated) {
// Check if x-spring-paginated is not explicitly set to false
if (operation.getExtensions() == null || !Boolean.FALSE.equals(operation.getExtensions().get("x-spring-paginated"))) {


if (hasParamsForPageable) {
// Automatically add x-spring-paginated to the operation
if (operation.getExtensions() == null) {
operation.setExtensions(new HashMap<>());
}
operation.getExtensions().put("x-spring-paginated", Boolean.TRUE);
codegenOperation.vendorExtensions.put("x-spring-paginated", Boolean.TRUE);
}
}
}

// Only process x-spring-paginated for server-side libraries (spring-boot)
// Client libraries (spring-cloud, spring-declarative-http-interface) need actual query parameters for HTTP requests
if (SPRING_BOOT.equals(library)) {
// add Pageable import only if x-spring-paginated explicitly used AND it's a server library
// this allows to use a custom Pageable schema without importing Spring Pageable.
if (operation.getExtensions() != null && Boolean.TRUE.equals(operation.getExtensions().get("x-spring-paginated"))) {
importMapping.putIfAbsent("Pageable", "org.springframework.data.domain.Pageable");
}

// add org.springframework.data.domain.Pageable import when needed (server libraries only)
if (operation.getExtensions() != null && Boolean.TRUE.equals(operation.getExtensions().get("x-spring-paginated"))) {
codegenOperation.imports.add("Pageable");
if (DocumentationProvider.SPRINGFOX.equals(getDocumentationProvider())) {
codegenOperation.imports.add("ApiIgnore");
}
if (DocumentationProvider.SPRINGDOC.equals(getDocumentationProvider())) {
codegenOperation.imports.add("PageableAsQueryParam");
// Prepend @PageableAsQueryParam to existing x-operation-extra-annotation if present
// Use getObjectAsStringList to properly handle both list and string formats:
// - YAML list: ['@Ann1', '@Ann2'] -> List of annotations
// - Single string: '@Ann1 @Ann2' -> Single-element list
// - Nothing/null -> Empty list
Object existingAnnotation = codegenOperation.vendorExtensions.get("x-operation-extra-annotation");
List<String> annotations = DefaultCodegen.getObjectAsStringList(existingAnnotation);

// Prepend @PageableAsQueryParam to the beginning of the list
List<String> updatedAnnotations = new ArrayList<>();
updatedAnnotations.add("@PageableAsQueryParam");
updatedAnnotations.addAll(annotations);

codegenOperation.vendorExtensions.put("x-operation-extra-annotation", updatedAnnotations);
}

// #8315 Remove matching Spring Data Web default query params if 'x-spring-paginated' with Pageable is used
codegenOperation.queryParams.removeIf(param -> defaultPageableQueryParams.contains(param.baseName));
codegenOperation.allParams.removeIf(param -> param.isQueryParam && defaultPageableQueryParams.contains(param.baseName));
}
}
return codegenOperation;
}

@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
super.preprocessOpenAPI(openAPI);
Expand Down Expand Up @@ -1117,12 +1234,14 @@ public List<VendorExtension> getSupportedVendorExtensions() {
extensions.add(VendorExtension.X_CONTENT_TYPE);
extensions.add(VendorExtension.X_DISCRIMINATOR_VALUE);
extensions.add(VendorExtension.X_FIELD_EXTRA_ANNOTATION);
extensions.add(VendorExtension.X_OPERATION_EXTRA_ANNOTATION);
extensions.add(VendorExtension.X_PATTERN_MESSAGE);
extensions.add(VendorExtension.X_SIZE_MESSAGE);
extensions.add(VendorExtension.X_MINIMUM_MESSAGE);
extensions.add(VendorExtension.X_MAXIMUM_MESSAGE);
extensions.add(VendorExtension.X_KOTLIN_IMPLEMENTS);
extensions.add(VendorExtension.X_KOTLIN_IMPLEMENTS_FIELDS);
extensions.add(VendorExtension.X_SPRING_PAGINATED);
return extensions;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v
authorizations = [{{#authMethods}}Authorization(value = "{{name}}"{{#isOAuth}}, scopes = [{{#scopes}}AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}}, {{/-last}}{{/scopes}}]{{/isOAuth}}){{^-last}}, {{/-last}}{{/authMethods}}]{{/hasAuthMethods}})
@ApiResponses(
value = [{{#responses}}ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}::class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}{{/responses}}]){{/swagger1AnnotationLibrary}}
{{#vendorExtensions.x-operation-extra-annotation}}
{{{.}}}
{{/vendorExtensions.x-operation-extra-annotation}}
@RequestMapping(
method = [RequestMethod.{{httpMethod}}],
// "{{#lambdaEscapeInNormalString}}{{{path}}}{{/lambdaEscapeInNormalString}}"
Expand All @@ -95,7 +98,9 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v
)
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}
{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}},
{{/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}}
{{/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}},
{{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},
{{/includeHttpRequestContext}}{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Pageable parameter is added with @Parameter(hidden = true) but no @ParameterObject or explicit pagination parameters are added, so enabling x-spring-paginated will hide pagination params from generated OpenAPI docs. Springdoc expects @ParameterObject (or explicit query params) to expose page/size/sort; hiding the pageable without those means pagination won’t appear in the docs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache, line 100:

<comment>The Pageable parameter is added with `@Parameter(hidden = true)` but no `@ParameterObject` or explicit pagination parameters are added, so enabling x-spring-paginated will hide pagination params from generated OpenAPI docs. Springdoc expects `@ParameterObject` (or explicit query params) to expose page/size/sort; hiding the pageable without those means pagination won’t appear in the docs.</comment>

<file context>
@@ -95,7 +95,9 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v
-        {{/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}}
+        {{/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}},
+        {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},
+        {{/includeHttpRequestContext}}{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}}
     {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}} {
         return {{>returnValue}}
</file context>
Fix with Cubic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @cubic-dev-ai I think this is fixed now that I am conditionally adding the @PageableAsQueryParam to the method.

{{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}} {
return {{>returnValue}}
}
Expand Down
Loading
Loading