Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
473c551
feat: add sort validation support for pageable operations
Picazsoo Apr 10, 2026
e3bb3ad
add sample
Picazsoo Apr 10, 2026
8ebb3cf
add generated samples
Picazsoo Apr 10, 2026
c751e31
add to compiled samples
Picazsoo Apr 10, 2026
58fe32b
add to compiled samples
Picazsoo Apr 10, 2026
033bbb1
regenerate docs
Picazsoo Apr 12, 2026
5981639
feat: add pageable defaults validation for pageable operations
Picazsoo Apr 13, 2026
919ee18
feat: enhance pageable validation with sort enum support
Picazsoo Apr 13, 2026
9af137f
feat: add pageable constraint validation tests for size and page limits
Picazsoo Apr 14, 2026
88fbea3
update samples
Picazsoo Apr 14, 2026
67884e8
update samples & fix test
Picazsoo Apr 14, 2026
767f864
feat: add validation for pageable and sort parameters with new annota…
Picazsoo Apr 14, 2026
32b6c4a
simplify implementation
Picazsoo Apr 14, 2026
9728c9a
improve doc description
Picazsoo Apr 14, 2026
f1935b4
improve sample
Picazsoo Apr 14, 2026
f4bd267
Merge branch 'master' into feature/add-enum-validation-for-pageable
Picazsoo Apr 15, 2026
8cb9f14
rename from non-existent 'beanValidations' to 'useBeanValidation'
Picazsoo Apr 15, 2026
8aad0ee
feat: add enum validation for pageable sort parameters in API
Picazsoo Apr 15, 2026
e040bec
add tests for both exploded and non-exploded params
Picazsoo Apr 15, 2026
1446407
fix samples after fix
Picazsoo Apr 15, 2026
333143c
add spring-boot-starter-validation when useBeanValidation
Picazsoo Apr 15, 2026
3e30230
add java spring test and delete incorrect sample
Picazsoo Apr 15, 2026
ce62f22
modify sample
Picazsoo Apr 15, 2026
bf56873
add tests
Picazsoo Apr 15, 2026
618d868
add java samples
Picazsoo Apr 15, 2026
701b035
add java samples
Picazsoo Apr 15, 2026
204b9f6
add tests to java samples
Picazsoo Apr 15, 2026
e4f8285
add missing tests for autoXSpringPaginated in java spring
Picazsoo Apr 15, 2026
6be3da3
add to built samples
Picazsoo Apr 15, 2026
cf212aa
fix: guard ValidPageable validator against Pageable.unpaged()
Picazsoo Apr 15, 2026
a701da6
fix javadoc
Picazsoo Apr 15, 2026
507128f
remove unused import
Picazsoo Apr 15, 2026
b02ca92
Merge remote-tracking branch 'origin/feature/add-enum-validation-for-…
Picazsoo Apr 16, 2026
7a21a49
whitespace change to re-run tests
Picazsoo Apr 17, 2026
4011e14
Revert "whitespace change to re-run tests"
Picazsoo Apr 17, 2026
323adf1
try to add exponential retry to the apt-get
Picazsoo Apr 17, 2026
879abcd
Merge branch 'master' into feature/add-enum-validation-for-pageable
Picazsoo Apr 20, 2026
87b1291
update samples
Picazsoo Apr 20, 2026
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 .github/workflows/samples-kotlin-server-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
- 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**'
- 'samples/server/petstore/kotlin-spring-declarative*/**'
- 'samples/server/petstore/kotlin-spring-sealed-interfaces/**'
- 'samples/server/petstore/kotlin-springboot-sort-validation/**'
# comment out due to gradle build failure
# - samples/server/petstore/kotlin-spring-default/**
pull_request:
Expand All @@ -23,6 +24,7 @@ on:
- 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**'
- 'samples/server/petstore/kotlin-spring-declarative*/**'
- 'samples/server/petstore/kotlin-spring-sealed-interfaces/**'
- 'samples/server/petstore/kotlin-springboot-sort-validation/**'
# comment out due to gradle build failure
# - samples/server/petstore/kotlin-spring-default/**

Expand Down Expand Up @@ -62,6 +64,7 @@ jobs:
- samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped
- samples/server/petstore/kotlin-spring-declarative-interface-wrapped
- samples/server/petstore/kotlin-spring-sealed-interfaces
- samples/server/petstore/kotlin-springboot-sort-validation
# comment out due to gradle build failure
# - samples/server/petstore/kotlin-spring-default/
steps:
Expand Down
16 changes: 16 additions & 0 deletions bin/configs/kotlin-spring-boot-sort-validation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
generatorName: kotlin-spring
outputDir: samples/server/petstore/kotlin-springboot-sort-validation
library: spring-boot
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
additionalProperties:
documentationProvider: none
annotationLibrary: none
useSwaggerUI: "false"
serviceImplementation: "true"
serializableModel: "true"
beanValidations: "true"
useSpringBoot3: "true"
generateSortValidation: "true"
useTags: "true"
requestMappingMode: api_interface
2 changes: 2 additions & 0 deletions docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|documentationProvider|Select the OpenAPI documentation provider.|<dl><dt>**none**</dt><dd>Do not publish an OpenAPI specification.</dd><dt>**source**</dt><dd>Publish the original input OpenAPI specification.</dd><dt>**springdoc**</dt><dd>Generate an OpenAPI 3 specification using SpringDoc.</dd></dl>|springdoc|
|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |original|
|exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true|
|generatePageableConstraintValidation|Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to paginated operations whose 'page' or 'size' parameter has a maximum constraint. Requires useBeanValidation=true and library=spring-boot.| |false|
|generateSortValidation|Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to paginated operations whose 'sort' parameter has enum values. Requires useBeanValidation=true and library=spring-boot.| |false|
|gradleBuildFile|generate a gradle build file using the Kotlin DSL| |true|
|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
|implicitHeaders|Skip header parameters in the generated API methods.| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import com.samskivert.mustache.Template;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import lombok.Getter;
import lombok.Setter;
import org.openapitools.codegen.*;
Expand Down Expand Up @@ -98,6 +101,8 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
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";
public static final String GENERATE_SORT_VALIDATION = "generateSortValidation";
public static final String GENERATE_PAGEABLE_CONSTRAINT_VALIDATION = "generatePageableConstraintValidation";
public static final String USE_SEALED_RESPONSE_INTERFACES = "useSealedResponseInterfaces";
public static final String COMPANION_OBJECT = "companionObject";

Expand Down Expand Up @@ -164,6 +169,8 @@ public String getDescription() {
@Setter private DeclarativeInterfaceReactiveMode declarativeInterfaceReactiveMode = DeclarativeInterfaceReactiveMode.coroutines;
@Setter private boolean useResponseEntity = true;
@Setter private boolean autoXSpringPaginated = false;
@Setter private boolean generateSortValidation = false;
@Setter private boolean generatePageableConstraintValidation = false;
@Setter private boolean useSealedResponseInterfaces = false;
@Setter private boolean companionObject = false;

Expand All @@ -180,6 +187,15 @@ public String getDescription() {
private Map<String, String> sealedInterfaceToOperationId = new HashMap<>();
private boolean sealedInterfacesFileWritten = false;

// Map from operationId to allowed sort values for @ValidSort annotation generation
private Map<String, List<String>> sortValidationEnums = new HashMap<>();

// Map from operationId to pageable defaults for @PageableDefault/@SortDefault annotation generation
private Map<String, SpringPageableScanUtils.PageableDefaultsData> pageableDefaultsRegistry = new HashMap<>();

// Map from operationId to pageable constraints for @ValidPageable annotation generation
private Map<String, SpringPageableScanUtils.PageableConstraintsData> pageableConstraintsRegistry = new HashMap<>();

public KotlinSpringServerCodegen() {
super();

Expand Down Expand Up @@ -272,6 +288,8 @@ public KotlinSpringServerCodegen() {
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);
addSwitch(GENERATE_SORT_VALIDATION, "Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to paginated operations whose 'sort' parameter has enum values. Requires useBeanValidation=true and library=spring-boot.", generateSortValidation);
addSwitch(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to paginated operations whose 'page' or 'size' parameter has a maximum constraint. Requires useBeanValidation=true and library=spring-boot.", generatePageableConstraintValidation);
addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject);
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
Expand Down Expand Up @@ -704,6 +722,14 @@ public void processOpts() {
this.setAutoXSpringPaginated(convertPropertyToBoolean(AUTO_X_SPRING_PAGINATED));
}
writePropertyBack(AUTO_X_SPRING_PAGINATED, autoXSpringPaginated);
if (additionalProperties.containsKey(GENERATE_SORT_VALIDATION) && library.equals(SPRING_BOOT)) {
this.setGenerateSortValidation(convertPropertyToBoolean(GENERATE_SORT_VALIDATION));
}
writePropertyBack(GENERATE_SORT_VALIDATION, generateSortValidation);
if (additionalProperties.containsKey(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION) && library.equals(SPRING_BOOT)) {
this.setGeneratePageableConstraintValidation(convertPropertyToBoolean(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION));
}
writePropertyBack(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, generatePageableConstraintValidation);
if (isUseSpringBoot3() && isUseSpringBoot4()) {
throw new IllegalArgumentException("Choose between Spring Boot 3 and Spring Boot 4");
}
Expand Down Expand Up @@ -1042,6 +1068,52 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
}

// #8315 Remove matching Spring Data Web default query params if 'x-spring-paginated' with Pageable is used
// Build pageable parameter annotations (@ValidPageable, @ValidSort, @PageableDefault, @SortDefault.SortDefaults)
List<String> pageableAnnotations = new ArrayList<>();

if (generatePageableConstraintValidation && useBeanValidation && pageableConstraintsRegistry.containsKey(codegenOperation.operationId)) {
SpringPageableScanUtils.PageableConstraintsData constraints = pageableConstraintsRegistry.get(codegenOperation.operationId);
List<String> attrs = new ArrayList<>();
if (constraints.maxSize >= 0) attrs.add("maxSize = " + constraints.maxSize);
if (constraints.maxPage >= 0) attrs.add("maxPage = " + constraints.maxPage);
pageableAnnotations.add("@ValidPageable(" + String.join(", ", attrs) + ")");
codegenOperation.imports.add("ValidPageable");
}

if (generateSortValidation && useBeanValidation && sortValidationEnums.containsKey(codegenOperation.operationId)) {
List<String> allowedSortValues = sortValidationEnums.get(codegenOperation.operationId);
String allowedValuesStr = allowedSortValues.stream()
.map(v -> "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\"")
.collect(Collectors.joining(", "));
pageableAnnotations.add("@ValidSort(allowedValues = [" + allowedValuesStr + "])");
codegenOperation.imports.add("ValidSort");
}

// Generate @PageableDefault / @SortDefault.SortDefaults annotations if defaults are present
if (pageableDefaultsRegistry.containsKey(codegenOperation.operationId)) {
SpringPageableScanUtils.PageableDefaultsData defaults = pageableDefaultsRegistry.get(codegenOperation.operationId);

if (defaults.page != null || defaults.size != null) {
List<String> attrs = new ArrayList<>();
if (defaults.page != null) attrs.add("page = " + defaults.page);
if (defaults.size != null) attrs.add("size = " + defaults.size);
pageableAnnotations.add("@PageableDefault(" + String.join(", ", attrs) + ")");
codegenOperation.imports.add("PageableDefault");
}

if (!defaults.sortDefaults.isEmpty()) {
List<String> sortEntries = defaults.sortDefaults.stream()
.map(sf -> "SortDefault(sort = [\"" + sf.field + "\"], direction = Sort.Direction." + sf.direction + ")")
.collect(Collectors.toList());
pageableAnnotations.add("@SortDefault.SortDefaults(" + String.join(", ", sortEntries) + ")");
codegenOperation.imports.add("SortDefault");
codegenOperation.imports.add("Sort");
}
}

if (!pageableAnnotations.isEmpty()) {
codegenOperation.vendorExtensions.put("x-pageable-extra-annotation", pageableAnnotations);
}
codegenOperation.queryParams.removeIf(param -> defaultPageableQueryParams.contains(param.baseName));
codegenOperation.allParams.removeIf(param -> param.isQueryParam && defaultPageableQueryParams.contains(param.baseName));
}
Expand All @@ -1058,6 +1130,33 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.kt"));
}

if (SPRING_BOOT.equals(library) && generateSortValidation && useBeanValidation) {
sortValidationEnums = SpringPageableScanUtils.scanSortValidationEnums(openAPI, autoXSpringPaginated);
if (!sortValidationEnums.isEmpty()) {
importMapping.putIfAbsent("ValidSort", configPackage + ".ValidSort");
supportingFiles.add(new SupportingFile("validSort.mustache",
(sourceFolder + File.separator + configPackage).replace(".", File.separator), "ValidSort.kt"));
}
}

if (SPRING_BOOT.equals(library)) {
pageableDefaultsRegistry = SpringPageableScanUtils.scanPageableDefaults(openAPI, autoXSpringPaginated);
if (!pageableDefaultsRegistry.isEmpty()) {
importMapping.putIfAbsent("PageableDefault", "org.springframework.data.web.PageableDefault");
importMapping.putIfAbsent("SortDefault", "org.springframework.data.web.SortDefault");
importMapping.putIfAbsent("Sort", "org.springframework.data.domain.Sort");
}
}

if (SPRING_BOOT.equals(library) && generatePageableConstraintValidation && useBeanValidation) {
pageableConstraintsRegistry = SpringPageableScanUtils.scanPageableConstraints(openAPI, autoXSpringPaginated);
if (!pageableConstraintsRegistry.isEmpty()) {
importMapping.putIfAbsent("ValidPageable", configPackage + ".ValidPageable");
supportingFiles.add(new SupportingFile("validPageable.mustache",
(sourceFolder + File.separator + configPackage).replace(".", File.separator), "ValidPageable.kt"));
}
}

if (!additionalProperties.containsKey(TITLE)) {
// The purpose of the title is for:
// - README documentation
Expand Down Expand Up @@ -1123,6 +1222,14 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
// TODO: Handle tags
}

/**
* Returns true if the given operation will have a Pageable parameter injected.
* Delegates to {@link SpringPageableScanUtils#willBePageable}.
*/
private boolean willBePageable(Operation operation) {
return SpringPageableScanUtils.willBePageable(operation, autoXSpringPaginated);
}

@Override
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
super.postProcessModelProperty(model, property);
Expand Down
Loading
Loading