Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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}}
{{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}} {
return {{>returnValue}}
}
Expand Down
Loading
Loading