pathEntry : openAPI.getPaths().entrySet()) {
+ for (Operation operation : pathEntry.getValue().readOperations()) {
+ String operationId = operation.getOperationId();
+ if (operationId == null || !willBePageable(operation, autoXSpringPaginated)) {
+ continue;
+ }
+ if (operation.getParameters() == null) {
+ continue;
+ }
+ int maxPage = -1;
+ int maxSize = -1;
+ for (Parameter param : operation.getParameters()) {
+ Schema> schema = param.getSchema();
+ if (schema == null) {
+ continue;
+ }
+ if (schema.get$ref() != null) {
+ schema = ModelUtils.getReferencedSchema(openAPI, schema);
+ }
+ if (schema == null || schema.getMaximum() == null) {
+ continue;
+ }
+ int maximum = schema.getMaximum().intValue();
+ switch (param.getName()) {
+ case "page":
+ maxPage = maximum;
+ break;
+ case "size":
+ maxSize = maximum;
+ break;
+ default:
+ break;
+ }
+ }
+ PageableConstraintsData data = new PageableConstraintsData(maxPage, maxSize);
+ if (data.hasAny()) {
+ result.put(operationId, data);
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache
index 2f05bbace9ca..5ac4a949ca1d 100644
--- a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache
@@ -279,7 +279,7 @@ public interface {{classname}} {
{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{>cookieParams}}{{^-last}},
{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}},
{{/hasParams}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true){{/swagger2AnnotationLibrary}} final {{#reactive}}ServerWebExchange exchange{{/reactive}}{{^reactive}}HttpServletRequest servletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
- {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},{{/includeHttpRequestContext}}{{/hasParams}}{{#springDocDocumentationProvider}}@ParameterObject{{/springDocDocumentationProvider}} final Pageable pageable{{/vendorExtensions.x-spring-paginated}}{{#vendorExtensions.x-spring-provide-args}}{{#hasParams}},
+ {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},{{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#springDocDocumentationProvider}}@ParameterObject{{/springDocDocumentationProvider}} final Pageable pageable{{/vendorExtensions.x-spring-paginated}}{{#vendorExtensions.x-spring-provide-args}}{{#hasParams}},
{{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},{{/includeHttpRequestContext}}{{/hasParams}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true){{/swagger2AnnotationLibrary}} {{{.}}}{{^hasParams}}{{^-last}}{{^reactive}},{{/reactive}}
{{/-last}}{{/hasParams}}{{/vendorExtensions.x-spring-provide-args}}
){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} {
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/validPageable.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/validPageable.mustache
new file mode 100644
index 000000000000..daf547481640
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/validPageable.mustache
@@ -0,0 +1,99 @@
+package {{configPackage}};
+
+import {{javaxPackage}}.validation.Constraint;
+import {{javaxPackage}}.validation.ConstraintValidator;
+import {{javaxPackage}}.validation.ConstraintValidatorContext;
+import {{javaxPackage}}.validation.Payload;
+import org.springframework.data.domain.Pageable;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Validates that the page number and page size in the annotated {@link Pageable} parameter do not
+ * exceed their configured maximums.
+ *
+ * Apply directly on a {@code Pageable} parameter. Each attribute is independently optional:
+ *
+ * - {@link #maxSize()} — when set (>= 0), validates {@code pageable.getPageSize() <= maxSize}
+ *
- {@link #maxPage()} — when set (>= 0), validates {@code pageable.getPageNumber() <= maxPage}
+ *
+ *
+ * Use {@link #NO_LIMIT} (= {@code -1}, the default) to leave an attribute unconstrained.
+ *
+ *
Constraining {@link #maxPage()} is useful to prevent deep-pagination attacks, where a large
+ * page offset (e.g. {@code ?page=100000&size=20}) causes an expensive {@code OFFSET} query on the
+ * database.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = {ValidPageable.PageableConstraintValidator.class})
+@Target({ElementType.PARAMETER})
+public @interface ValidPageable {
+
+ /** Sentinel value meaning no limit is applied. */
+ int NO_LIMIT = -1;
+
+ /** Maximum allowed page size, or {@link #NO_LIMIT} if unconstrained. */
+ int maxSize() default NO_LIMIT;
+
+ /** Maximum allowed page number (0-based), or {@link #NO_LIMIT} if unconstrained. */
+ int maxPage() default NO_LIMIT;
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+ String message() default "Invalid page request";
+
+ class PageableConstraintValidator implements ConstraintValidator {
+
+ private int maxSize = NO_LIMIT;
+ private int maxPage = NO_LIMIT;
+
+ @Override
+ public void initialize(ValidPageable constraintAnnotation) {
+ maxSize = constraintAnnotation.maxSize();
+ maxPage = constraintAnnotation.maxPage();
+ }
+
+ @Override
+ public boolean isValid(Pageable pageable, ConstraintValidatorContext context) {
+ if (pageable == null) {
+ return true;
+ }
+
+ if (!pageable.isPaged()) {
+ return true;
+ }
+
+ boolean valid = true;
+ context.disableDefaultConstraintViolation();
+
+ if (maxSize >= 0 && pageable.getPageSize() > maxSize) {
+ context.buildConstraintViolationWithTemplate(
+ context.getDefaultConstraintMessageTemplate()
+ + ": page size " + pageable.getPageSize()
+ + " exceeds maximum " + maxSize)
+ .addPropertyNode("size")
+ .addConstraintViolation();
+ valid = false;
+ }
+
+ if (maxPage >= 0 && pageable.getPageNumber() > maxPage) {
+ context.buildConstraintViolationWithTemplate(
+ context.getDefaultConstraintMessageTemplate()
+ + ": page number " + pageable.getPageNumber()
+ + " exceeds maximum " + maxPage)
+ .addPropertyNode("page")
+ .addConstraintViolation();
+ valid = false;
+ }
+
+ return valid;
+ }
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/validSort.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/validSort.mustache
new file mode 100644
index 000000000000..6b47813dd477
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/validSort.mustache
@@ -0,0 +1,102 @@
+package {{configPackage}};
+
+import {{javaxPackage}}.validation.Constraint;
+import {{javaxPackage}}.validation.ConstraintValidator;
+import {{javaxPackage}}.validation.ConstraintValidatorContext;
+import {{javaxPackage}}.validation.Payload;
+import org.springframework.data.domain.Pageable;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/**
+ * Validates that sort properties in the annotated {@link Pageable} parameter match the allowed values.
+ *
+ * Apply directly on a {@code Pageable} parameter. The validator checks that each sort
+ * property and direction combination in the {@link Pageable} matches one of the strings specified
+ * in {@link #allowedValues()}.
+ *
+ *
Two formats are accepted in {@link #allowedValues()}:
+ *
+ * - {@code "property,direction"} — permits only the specific direction (e.g. {@code "id,asc"},
+ * {@code "name,desc"}). Direction matching is case-insensitive.
+ *
- {@code "property"} — permits any direction for that property (e.g. {@code "id"} matches
+ * {@code sort=id,asc} and {@code sort=id,desc}). Note: because Spring always normalises a
+ * bare {@code sort=id} to ascending before the validator runs, bare property names in
+ * {@link #allowedValues()} effectively allow all directions.
+ *
+ *
+ * Both formats may be mixed freely. For example {@code {"id", "name,desc"}} allows {@code id}
+ * in any direction but restricts {@code name} to descending only.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = {ValidSort.SortValidator.class})
+@Target({ElementType.PARAMETER})
+public @interface ValidSort {
+
+ /** The allowed sort strings (e.g. {@code {"id,asc", "id,desc"}}). */
+ String[] allowedValues();
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+ String message() default "Invalid sort column";
+
+ class SortValidator implements ConstraintValidator {
+
+ private Set allowedValues;
+
+ @Override
+ public void initialize(ValidSort constraintAnnotation) {
+ allowedValues = Arrays.stream(constraintAnnotation.allowedValues())
+ .map(entry -> entry
+ .replaceAll("(?i),ASC$", ",asc")
+ .replaceAll("(?i),DESC$", ",desc"))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public boolean isValid(Pageable pageable, ConstraintValidatorContext context) {
+ if (pageable == null || pageable.getSort().isUnsorted()) {
+ return true;
+ }
+
+ Map invalid = new TreeMap<>();
+ int[] index = {0};
+ pageable.getSort().forEach(order -> {
+ String sortValue = order.getProperty() + "," + order.getDirection().name().toLowerCase(java.util.Locale.ROOT);
+ // Accept "property,direction" (exact match) OR "property" alone (any direction allowed)
+ if (!allowedValues.contains(sortValue) && !allowedValues.contains(order.getProperty())) {
+ invalid.put(index[0], order.getProperty());
+ }
+ index[0]++;
+ });
+
+ if (!invalid.isEmpty()) {
+ context.disableDefaultConstraintViolation();
+ invalid.forEach((i, property) ->
+ context.buildConstraintViolationWithTemplate(
+ context.getDefaultConstraintMessageTemplate() + " [" + property + "]")
+ .addPropertyNode("sort")
+ .addPropertyNode("property")
+ .inIterable()
+ .atIndex(i)
+ .addConstraintViolation());
+ }
+
+ return invalid.isEmpty();
+ }
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache
index 253cd1719766..033d044fb28e 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache
@@ -110,7 +110,7 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v
{{>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}}{{#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}}
+ {{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#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}}
}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache
index 04232e570533..795070dde368 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache
@@ -125,7 +125,7 @@ interface {{classname}} {
{{>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}}{{#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}}
+ {{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}}
{{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{#useResponseEntity}}>{{/useResponseEntity}}{{^skipDefaultApiInterface}} {
{{^isDelegate}}
return {{>returnValue}}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb3-Kts.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb3-Kts.mustache
index c1fb61e35c18..6264845e18b8 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb3-Kts.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb3-Kts.mustache
@@ -50,6 +50,7 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
{{#useBeanValidation}}
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache
index c9c5996d0e09..893231362d49 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache
@@ -58,6 +58,7 @@ dependencies {
{{/useJackson3}}
implementation("org.springframework.data:spring-data-commons")
{{#useBeanValidation}}
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradleKts.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradleKts.mustache
index fe0ff44f0d34..ec356c78edea 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradleKts.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradleKts.mustache
@@ -57,6 +57,7 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
{{#useBeanValidation}}
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api"){{/useBeanValidation}}
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb3.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb3.mustache
index ea1ac4523871..0019fc4b0b37 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb3.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb3.mustache
@@ -192,6 +192,10 @@
jakarta.validation
jakarta.validation-api
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
{{/useBeanValidation}}
jakarta.annotation
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache
index 9d249a18caac..18c2c5b22c5c 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache
@@ -179,6 +179,10 @@
jakarta.validation
jakarta.validation-api
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
{{/useBeanValidation}}
jakarta.annotation
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom.mustache
index 06d98458f75d..8765049c3494 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom.mustache
@@ -179,6 +179,10 @@
javax.validation
validation-api
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
{{/useBeanValidation}}
javax.annotation
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/validPageable.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/validPageable.mustache
new file mode 100644
index 000000000000..6b26b7a26803
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/validPageable.mustache
@@ -0,0 +1,81 @@
+package {{configPackage}}
+
+import {{javaxPackage}}.validation.Constraint
+import {{javaxPackage}}.validation.ConstraintValidator
+import {{javaxPackage}}.validation.ConstraintValidatorContext
+import {{javaxPackage}}.validation.Payload
+import org.springframework.data.domain.Pageable
+
+/**
+ * Validates that the page number and page size in the annotated [Pageable] parameter do not
+ * exceed their configured maximums.
+ *
+ * Apply directly on a `pageable: Pageable` parameter. Each attribute is independently optional:
+ * - [maxSize] — when set (>= 0), validates `pageable.pageSize <= maxSize`
+ * - [maxPage] — when set (>= 0), validates `pageable.pageNumber <= maxPage`
+ *
+ * Use [NO_LIMIT] (= -1, the default) to leave an attribute unconstrained.
+ *
+ * Constraining [maxPage] is useful to prevent deep-pagination attacks, where a large page
+ * offset (e.g. `?page=100000&size=20`) causes an expensive `OFFSET` query on the database.
+ *
+ * @property maxSize Maximum allowed page size, or [NO_LIMIT] if unconstrained
+ * @property maxPage Maximum allowed page number (0-based), or [NO_LIMIT] if unconstrained
+ * @property groups Validation groups (optional)
+ * @property payload Additional payload (optional)
+ * @property message Validation error message (default: "Invalid page request")
+ */
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+@Constraint(validatedBy = [PageableConstraintValidator::class])
+@Target(AnnotationTarget.VALUE_PARAMETER)
+annotation class ValidPageable(
+ val maxSize: Int = ValidPageable.NO_LIMIT,
+ val maxPage: Int = ValidPageable.NO_LIMIT,
+ val groups: Array> = [],
+ val payload: Array> = [],
+ val message: String = "Invalid page request"
+) {
+ companion object {
+ const val NO_LIMIT = -1
+ }
+}
+
+class PageableConstraintValidator : ConstraintValidator {
+
+ private var maxSize = ValidPageable.NO_LIMIT
+ private var maxPage = ValidPageable.NO_LIMIT
+
+ override fun initialize(constraintAnnotation: ValidPageable) {
+ maxSize = constraintAnnotation.maxSize
+ maxPage = constraintAnnotation.maxPage
+ }
+
+ override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean {
+ if (pageable == null) return true
+ if (!pageable.isPaged) return true
+
+ var valid = true
+ context.disableDefaultConstraintViolation()
+
+ if (maxSize >= 0 && pageable.pageSize > maxSize) {
+ context.buildConstraintViolationWithTemplate(
+ "${context.defaultConstraintMessageTemplate}: page size ${pageable.pageSize} exceeds maximum $maxSize"
+ )
+ .addPropertyNode("size")
+ .addConstraintViolation()
+ valid = false
+ }
+
+ if (maxPage >= 0 && pageable.pageNumber > maxPage) {
+ context.buildConstraintViolationWithTemplate(
+ "${context.defaultConstraintMessageTemplate}: page number ${pageable.pageNumber} exceeds maximum $maxPage"
+ )
+ .addPropertyNode("page")
+ .addConstraintViolation()
+ valid = false
+ }
+
+ return valid
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/validSort.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/validSort.mustache
new file mode 100644
index 000000000000..7b3f7cf0adbd
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/validSort.mustache
@@ -0,0 +1,87 @@
+package {{configPackage}}
+
+import {{javaxPackage}}.validation.Constraint
+import {{javaxPackage}}.validation.ConstraintValidator
+import {{javaxPackage}}.validation.ConstraintValidatorContext
+import {{javaxPackage}}.validation.Payload
+import org.springframework.data.domain.Pageable
+
+/**
+ * Validates that sort properties in the annotated [Pageable] parameter match the allowed values.
+ *
+ * Apply directly on a `pageable: Pageable` parameter. The validator checks that each sort
+ * property and direction combination in the [Pageable] matches one of the strings specified
+ * in [allowedValues].
+ *
+ * Two formats are accepted in [allowedValues]:
+ * - `"property,direction"` — permits only the specific direction (e.g. `"id,asc"`, `"name,desc"`).
+ * Direction matching is case-insensitive: `"id,ASC"` and `"id,asc"` are treated identically.
+ * - `"property"` — permits any direction for that property (e.g. `"id"` matches `sort=id,asc`
+ * and `sort=id,desc`). Note: because Spring always normalises a bare `sort=id` to ascending
+ * before the validator runs, bare property names in [allowedValues] effectively allow all
+ * directions — the original omission of a direction cannot be detected.
+ *
+ * Both formats may be mixed freely. For example `["id", "name,desc"]` allows `id` in any
+ * direction but restricts `name` to descending only.
+ *
+ * @property allowedValues The allowed sort strings (e.g. `["id,asc", "id,desc"]`)
+ * @property groups Validation groups (optional)
+ * @property payload Additional payload (optional)
+ * @property message Validation error message (default: "Invalid sort column")
+ */
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+@Constraint(validatedBy = [SortValidator::class])
+@Target(AnnotationTarget.VALUE_PARAMETER)
+annotation class ValidSort(
+ val allowedValues: Array,
+ val groups: Array> = [],
+ val payload: Array> = [],
+ val message: String = "Invalid sort column"
+)
+
+class SortValidator : ConstraintValidator {
+
+ private lateinit var allowedValues: Set
+
+ override fun initialize(constraintAnnotation: ValidSort) {
+ allowedValues = constraintAnnotation.allowedValues.map { entry ->
+ DIRECTION_ASC_SUFFIX.replace(entry, ",asc")
+ .let { DIRECTION_DESC_SUFFIX.replace(it, ",desc") }
+ }.toSet()
+ }
+
+ private companion object {
+ val DIRECTION_ASC_SUFFIX = Regex(",ASC$", RegexOption.IGNORE_CASE)
+ val DIRECTION_DESC_SUFFIX = Regex(",DESC$", RegexOption.IGNORE_CASE)
+ }
+
+ override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean {
+ if (pageable == null || pageable.sort.isUnsorted) return true
+
+ val invalid = pageable.sort
+ .foldIndexed(emptyMap()) { index, acc, order ->
+ val sortValue = "${order.property},${order.direction.name.lowercase()}"
+ // Accept "property,direction" (exact match) OR "property" alone (any direction allowed)
+ if (sortValue !in allowedValues && order.property !in allowedValues) acc + (index to order.property)
+ else acc
+ }
+ .toSortedMap()
+
+ if (invalid.isNotEmpty()) {
+ context.disableDefaultConstraintViolation()
+ invalid.forEach { (index, property) ->
+ context.buildConstraintViolationWithTemplate(
+ "${context.defaultConstraintMessageTemplate} [$property]"
+ )
+ .addPropertyNode("sort")
+ .addPropertyNode("property")
+ .inIterable()
+ .atIndex(index)
+ .addConstraintViolation()
+ }
+ }
+
+ return invalid.isEmpty()
+ }
+}
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 96444d9aee77..524dde54bb2e 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
@@ -6664,4 +6664,474 @@ public void testJspecify(String library, int springBootVersion, String fooApiFil
JavaFileAssert.assertThat(files.get("model/package-info.java"))
.fileContains("@org.jspecify.annotations.NullMarked");
}
+
+ // -------------------------------------------------------------------------
+ // autoXSpringPaginated tests
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void autoXSpringPaginatedDetectsAllThreeParams() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props);
+
+ // findPetsWithAutoDetect has page+size+sort → Pageable should be injected
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithAutoDetect")
+ .assertParameter("pageable").hasType("Pageable");
+ }
+
+ @Test
+ public void autoXSpringPaginatedManualFalseTakesPrecedence() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props);
+
+ // findPetsManualFalse has x-spring-paginated: false → Pageable must NOT be injected
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsManualFalse")
+ .doesNotHaveParameter("pageable");
+ }
+
+ @Test
+ public void autoXSpringPaginatedCaseSensitiveMatching() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props);
+
+ // findPetsCaseSensitive uses Page/Size/Sort (capital) → must NOT auto-detect
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsCaseSensitive")
+ .doesNotHaveParameter("pageable");
+ }
+
+ @Test
+ public void autoXSpringPaginatedNoDetectionWhenMissingPage() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props);
+
+ // findPetsMissingPage: missing 'page' param → Pageable must NOT be injected
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsMissingPage")
+ .doesNotHaveParameter("pageable");
+ }
+
+ @Test
+ public void autoXSpringPaginatedNoDetectionWhenMissingSize() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props);
+
+ // findPetsMissingSize: missing 'size' param → Pageable must NOT be injected
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsMissingSize")
+ .doesNotHaveParameter("pageable");
+ }
+
+ @Test
+ public void autoXSpringPaginatedNoDetectionWhenMissingSort() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props);
+
+ // findPetsMissingSort: missing 'sort' param → Pageable must NOT be injected
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsMissingSort")
+ .doesNotHaveParameter("pageable");
+ }
+
+ @Test
+ public void autoXSpringPaginatedOnlyForSpringBoot() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+
+ // spring-cloud generates a Feign client — auto-detect should not apply there
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", "spring-cloud", props);
+
+ File petApiClient = files.get("PetApiClient.java");
+ if (petApiClient != null) {
+ String content = java.nio.file.Files.readString(petApiClient.toPath());
+ assertThat(content).doesNotContain("Pageable pageable");
+ }
+ }
+
+ @Test
+ public void autoXSpringPaginatedDisabledByDefault() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ // NOT setting AUTO_X_SPRING_PAGINATED (defaults to false)
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props);
+
+ // findPetsWithAutoDetect: should NOT get Pageable when autoXSpringPaginated is not enabled
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithAutoDetect")
+ .doesNotHaveParameter("pageable");
+ }
+
+ @Test
+ public void autoXSpringPaginatedWorksWithManualTrue() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props);
+
+ // findPetsManualTrue: explicit x-spring-paginated: true → Pageable must be injected
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsManualTrue")
+ .assertParameter("pageable").hasType("Pageable");
+ }
+
+ @Test
+ public void autoXSpringPaginatedNoParamsDoesNotDetect() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props);
+
+ // findPetsNoParams: no params at all → Pageable must NOT be injected
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsNoParams")
+ .doesNotHaveParameter("pageable");
+ }
+
+ // -------------------------------------------------------------------------
+ // generateSortValidation tests
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void generateSortValidationAddsAnnotationAndGeneratesFile() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // ValidSort.java must be generated
+ assertThat(files).containsKey("ValidSort.java");
+
+ // findPetsWithSortEnum has explicit x-spring-paginated + sort enum → @ValidSort applied with all 4 values
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .fileContains("@ValidSort(allowedValues = {")
+ .fileContains("\"id,asc\"")
+ .fileContains("\"id,desc\"")
+ .fileContains("\"name,asc\"")
+ .fileContains("\"name,desc\"");
+ }
+
+ @Test
+ public void generateSortValidationUsesJavaArraySyntax() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // The generated API file must use Java {} array syntax (not Kotlin [])
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .fileContains("@ValidSort(allowedValues = {");
+ }
+
+ @Test
+ public void generateSortValidationWithAutoDetect() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
+ props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsAutoDetectedWithSort: auto-detected + sort enum → ValidSort applied with Java {} syntax
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .fileContains("@ValidSort(allowedValues = {")
+ .fileContains("\"id,asc\"")
+ .fileContains("\"id,desc\"");
+ }
+
+ @Test
+ public void generateSortValidationNotAppliedWhenNoSortEnum() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsWithoutSortEnum: paginated but sort has no enum → no @ValidSort
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithoutSortEnum")
+ .assertParameter("pageable")
+ .assertParameterAnnotations()
+ .doesNotContainWithName("ValidSort");
+ }
+
+ @Test
+ public void generateSortValidationWorksForArraySortEnum() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsWithArraySortEnum: sort is type:array, items have inline enum → @ValidSort with Java {} syntax
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithArraySortEnum")
+ .assertParameter("pageable")
+ .assertParameterAnnotations()
+ .containsWithName("ValidSort");
+
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .fileContains("@ValidSort(allowedValues = {")
+ .fileContains("\"id,asc\"")
+ .fileContains("\"id,desc\"")
+ .fileContains("\"name,asc\"")
+ .fileContains("\"name,desc\"");
+ }
+
+ @Test
+ public void generateSortValidationWorksForArraySortRefEnum() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsWithArraySortRefEnum: sort is type:array, items $ref to PetSort enum → @ValidSort with PetSort values
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithArraySortRefEnum")
+ .assertParameter("pageable")
+ .assertParameterAnnotations()
+ .containsWithName("ValidSort");
+
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .fileContains("\"id,asc\"")
+ .fileContains("\"id,desc\"")
+ .fileContains("\"createdAt,asc\"")
+ .fileContains("\"createdAt,desc\"");
+ }
+
+ @Test
+ public void generateSortValidationWorksForExternalParamRefArraySort() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsWithExternalParamRefArraySort: sort param $ref to external components file,
+ // type:array with items $ref to PetSortEnum in the same external file
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithExternalParamRefArraySort")
+ .assertParameter("pageable")
+ .assertParameterAnnotations()
+ .containsWithName("ValidSort");
+
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .fileContains("\"name,asc\"")
+ .fileContains("\"name,desc\"")
+ .fileContains("\"id,asc\"")
+ .fileContains("\"id,desc\"");
+ }
+
+ @Test
+ public void generateSortValidationWorksForNonExplodedExternalParamRefArraySort() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsWithNonExplodedExternalParamRefArraySort: sort param $ref to external file,
+ // explode: false — @ValidSort works identically since it validates the deserialized Pageable
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithNonExplodedExternalParamRefArraySort")
+ .assertParameter("pageable")
+ .assertParameterAnnotations()
+ .containsWithName("ValidSort");
+
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .fileContains("\"name,asc\"")
+ .fileContains("\"name,desc\"")
+ .fileContains("\"id,asc\"")
+ .fileContains("\"id,desc\"");
+ }
+
+ // -------------------------------------------------------------------------
+ // generatePageableConstraintValidation tests
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void generatePageableConstraintValidationAddsAnnotationAndGeneratesFile() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // ValidPageable.java must be generated
+ assertThat(files).containsKey("ValidPageable.java");
+
+ // findPetsWithSizeConstraint: size maximum=100 → @ValidPageable(maxSize = 100)
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithSizeConstraint")
+ .assertParameter("pageable")
+ .assertParameterAnnotations()
+ .containsWithNameAndAttributes("ValidPageable", Map.of("maxSize", "100"));
+ }
+
+ @Test
+ public void generatePageableConstraintValidationWithBothConstraints() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+ props.put(SpringCodegen.GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsWithPageAndSizeConstraint: page maximum=999, size maximum=50
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithPageAndSizeConstraint")
+ .assertParameter("pageable")
+ .assertParameterAnnotations()
+ .containsWithNameAndAttributes("ValidPageable", Map.of("maxSize", "50", "maxPage", "999"));
+ }
+
+ // -------------------------------------------------------------------------
+ // @PageableDefault / @SortDefault tests
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void pageableDefaultAnnotationApplied() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsWithPageSizeDefaultsOnly: page=0, size=25 → @PageableDefault(page = 0, size = 25)
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .assertMethod("findPetsWithPageSizeDefaultsOnly")
+ .assertParameter("pageable")
+ .assertParameterAnnotations()
+ .containsWithNameAndAttributes("PageableDefault", Map.of("page", "0", "size", "25"));
+ }
+
+ @Test
+ public void sortDefaultAnnotationApplied() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsWithSortDefaultOnly: sort default "name,desc" → @SortDefault.SortDefaults generated
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .fileContains("@SortDefault.SortDefaults({@SortDefault(sort = {\"name\"}, direction = Sort.Direction.DESC)})");
+ }
+
+ @Test
+ public void sortDefaultAndPageableDefaultBothApplied() throws IOException {
+ Map props = new HashMap<>();
+ props.put(SpringCodegen.INTERFACE_ONLY, "true");
+ props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
+ props.put(SpringCodegen.USE_TAGS, "true");
+ props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
+
+ Map files = generateFromContract(
+ "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props);
+
+ // findPetsWithAllDefaults: page=0, size=10, sort=["name,desc","id,asc"]
+ // → @PageableDefault + @SortDefault.SortDefaults both present
+ JavaFileAssert.assertThat(files.get("PetApi.java"))
+ .fileContains("@PageableDefault(page = 0, size = 10)")
+ .fileContains("@SortDefault.SortDefaults({@SortDefault(sort = {\"name\"}, direction = Sort.Direction.DESC), @SortDefault(sort = {\"id\"}, direction = Sort.Direction.ASC)})");
+ }
}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java
index 504583c759dd..d66ab3797e66 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java
@@ -3919,7 +3919,7 @@ public void springPaginatedNoParamsNoContext() throws Exception {
// Test operation listAllPets which has no parameters except pageable
File petApi = files.get("PetApi.kt");
- assertFileContains(petApi.toPath(), "fun listAllPets(@Parameter(hidden = true) pageable: Pageable)");
+ assertFileContains(petApi.toPath(), "fun listAllPets(@PageableDefault(page = 0, size = 20) @Parameter(hidden = true) pageable: Pageable)");
}
@Test
@@ -4160,8 +4160,459 @@ private Map generateFromContract(
.collect(Collectors.toMap(File::getName, Function.identity()));
}
+ // ========== GENERATE PAGEABLE CONSTRAINT VALIDATION TESTS ==========
+
+ @Test
+ public void generatePageableConstraintValidationAddsSizeConstraint() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ // findPetsWithSizeConstraint has maximum: 100 on size only
+ int methodStart = content.indexOf("fun findPetsWithSizeConstraint(");
+ Assert.assertTrue(methodStart >= 0, "findPetsWithSizeConstraint method should exist");
+ String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500));
+ Assert.assertTrue(paramBlock.contains("@ValidPageable(maxSize = 100)"),
+ "@ValidPageable(maxSize = 100) should appear on the pageable parameter");
+ Assert.assertFalse(paramBlock.contains("maxPage"),
+ "maxPage should not appear when only size has a maximum constraint");
+
+ assertFileContains(petApi.toPath(), "import org.openapitools.configuration.ValidPageable");
+ }
+
+ @Test
+ public void generatePageableConstraintValidationAddsPageAndSizeConstraint() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ // findPetsWithPageAndSizeConstraint has maximum: 999 on page and maximum: 50 on size
+ int methodStart = content.indexOf("fun findPetsWithPageAndSizeConstraint(");
+ Assert.assertTrue(methodStart >= 0, "findPetsWithPageAndSizeConstraint method should exist");
+ String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500));
+ Assert.assertTrue(paramBlock.contains("@ValidPageable(maxSize = 50, maxPage = 999)"),
+ "@ValidPageable(maxSize = 50, maxPage = 999) should appear on the pageable parameter");
+ }
+
+ @Test
+ public void generatePageableConstraintValidationGeneratesValidPageableFile() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File validPageableFile = files.get("ValidPageable.kt");
+ Assert.assertNotNull(validPageableFile, "ValidPageable.kt should be generated when generatePageableConstraintValidation=true");
+ assertFileContains(validPageableFile.toPath(), "annotation class ValidPageable");
+ assertFileContains(validPageableFile.toPath(), "class PageableConstraintValidator");
+ assertFileContains(validPageableFile.toPath(), "val maxSize: Int");
+ assertFileContains(validPageableFile.toPath(), "val maxPage: Int");
+ assertFileContains(validPageableFile.toPath(), "NO_LIMIT");
+ }
+
+ @Test
+ public void generatePageableConstraintValidationDoesNotGenerateFileWhenDisabled() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ // NOT setting GENERATE_PAGEABLE_CONSTRAINT_VALIDATION (defaults to false)
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ Assert.assertNull(files.get("ValidPageable.kt"), "ValidPageable.kt should NOT be generated when generatePageableConstraintValidation=false");
+ File petApi = files.get("PetApi.kt");
+ assertFileNotContains(petApi.toPath(), "@ValidPageable");
+ }
+
+ @Test
+ public void generatePageableConstraintValidationDoesNotGenerateFileWhenBeanValidationDisabled() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true");
+ additionalProperties.put(USE_BEANVALIDATION, "false");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ Assert.assertNull(files.get("ValidPageable.kt"), "ValidPageable.kt should NOT be generated when useBeanValidation=false");
+ File petApi = files.get("PetApi.kt");
+ assertFileNotContains(petApi.toPath(), "@ValidPageable");
+ }
+
// ========== AUTO X-SPRING-PAGINATED TESTS ==========
+ // ========== GENERATE SORT VALIDATION TESTS ==========
+
+ @Test
+ public void generateSortValidationAddsAnnotationForExplicitPaginated() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ assertFileContains(petApi.toPath(), "@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"name,asc\", \"name,desc\"])");
+ assertFileContains(petApi.toPath(), "import org.openapitools.configuration.ValidSort");
+
+ // @ValidSort must be a parameter annotation — appears in the 500-char window AFTER `fun findPetsWithSortEnum(`
+ String content = Files.readString(petApi.toPath());
+ int methodStart = content.indexOf("fun findPetsWithSortEnum(");
+ Assert.assertTrue(methodStart >= 0, "findPetsWithSortEnum method should exist");
+ String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500));
+ Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"name,asc\", \"name,desc\"])"),
+ "@ValidSort should appear as a parameter annotation (inside the method signature, after `fun`)");
+ Assert.assertTrue(paramBlock.contains("pageable: Pageable"),
+ "findPetsWithSortEnum should have a pageable: Pageable parameter");
+
+ // @ValidSort must NOT be a method-level annotation (not in the 500-char prefix before `fun`)
+ String prefixBlock = content.substring(Math.max(0, methodStart - 500), methodStart);
+ Assert.assertFalse(prefixBlock.contains("@ValidSort"),
+ "@ValidSort should be a parameter annotation, not a method-level annotation");
+ }
+
+ @Test
+ public void generateSortValidationAddsAnnotationForAutoDetectedPaginated() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+ additionalProperties.put(AUTO_X_SPRING_PAGINATED, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ assertFileContains(petApi.toPath(), "@ValidSort(allowedValues = [\"id,asc\", \"id,desc\"])");
+ }
+
+ @Test
+ public void generateSortValidationHandlesRefSortEnum() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ assertFileContains(petApi.toPath(), "@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"createdAt,asc\", \"createdAt,desc\"])");
+ }
+
+ @Test
+ public void generateSortValidationDoesNotAnnotateNonPaginatedOperation() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ // findPetsNonPaginatedWithSortEnum has sort enum but NO pagination — must not get @ValidSort
+ int methodStart = content.indexOf("fun findPetsNonPaginatedWithSortEnum(");
+ Assert.assertTrue(methodStart >= 0, "findPetsNonPaginatedWithSortEnum method should exist");
+ String methodBlock = content.substring(Math.max(0, methodStart - 500), methodStart);
+ Assert.assertFalse(methodBlock.contains("@ValidSort"),
+ "Non-paginated operation should not have @ValidSort even if sort param has enum values");
+ }
+
+ @Test
+ public void generateSortValidationDoesNotAnnotateWhenSortHasNoEnum() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ // findPetsWithoutSortEnum has pagination but sort has NO enum values
+ int methodStart = content.indexOf("fun findPetsWithoutSortEnum(");
+ Assert.assertTrue(methodStart >= 0, "findPetsWithoutSortEnum method should exist");
+ String methodBlock = content.substring(Math.max(0, methodStart - 500), methodStart);
+ Assert.assertFalse(methodBlock.contains("@ValidSort"),
+ "Paginated operation with non-enum sort should not have @ValidSort");
+ }
+
+ @Test
+ public void generateSortValidationGeneratesValidSortFile() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File validSortFile = files.get("ValidSort.kt");
+ Assert.assertNotNull(validSortFile, "ValidSort.kt should be generated when generateSortValidation=true");
+ assertFileContains(validSortFile.toPath(), "annotation class ValidSort");
+ assertFileContains(validSortFile.toPath(), "class SortValidator");
+ assertFileContains(validSortFile.toPath(), "val allowedValues: Array");
+ assertFileContains(validSortFile.toPath(), "DIRECTION_ASC_SUFFIX");
+ assertFileContains(validSortFile.toPath(), "DIRECTION_DESC_SUFFIX");
+ }
+
+ @Test
+ public void generateSortValidationDoesNotGenerateValidSortFileWhenDisabled() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ // NOT setting GENERATE_SORT_VALIDATION (defaults to false)
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ Assert.assertNull(files.get("ValidSort.kt"), "ValidSort.kt should NOT be generated when generateSortValidation=false");
+ File petApi = files.get("PetApi.kt");
+ assertFileNotContains(petApi.toPath(), "@ValidSort");
+ }
+
+ @Test
+ public void generateSortValidationDoesNotGenerateValidSortFileWhenBeanValidationDisabled() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+ additionalProperties.put(USE_BEANVALIDATION, "false");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ Assert.assertNull(files.get("ValidSort.kt"), "ValidSort.kt should NOT be generated when useBeanValidation=false");
+ File petApi = files.get("PetApi.kt");
+ assertFileNotContains(petApi.toPath(), "@ValidSort");
+ }
+
+ @Test
+ public void generateSortValidationWorksForArraySortEnum() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ // findPetsWithArraySortEnum: sort is type:array, items have inline enum → @ValidSort applied with Kotlin [] syntax
+ int methodStart = content.indexOf("fun findPetsWithArraySortEnum(");
+ Assert.assertTrue(methodStart >= 0, "findPetsWithArraySortEnum method should exist");
+ String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500));
+ Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"name,asc\", \"name,desc\"])"),
+ "@ValidSort with all four enum values should appear on the pageable parameter");
+ Assert.assertTrue(paramBlock.contains("pageable: Pageable"),
+ "findPetsWithArraySortEnum should have a pageable: Pageable parameter");
+ }
+
+ @Test
+ public void generateSortValidationWorksForArraySortRefEnum() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ // findPetsWithArraySortRefEnum: sort is type:array, items $ref to PetSort enum → @ValidSort with PetSort values
+ int methodStart = content.indexOf("fun findPetsWithArraySortRefEnum(");
+ Assert.assertTrue(methodStart >= 0, "findPetsWithArraySortRefEnum method should exist");
+ String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500));
+ Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"createdAt,asc\", \"createdAt,desc\"])"),
+ "@ValidSort with PetSort enum values should appear on the pageable parameter");
+ }
+
+ @Test
+ public void generateSortValidationWorksForExternalParamRefArraySort() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ // findPetsWithExternalParamRefArraySort: sort param $ref to external components file,
+ // which defines type:array with items $ref to PetSortEnum in the same external file
+ int methodStart = content.indexOf("fun findPetsWithExternalParamRefArraySort(");
+ Assert.assertTrue(methodStart >= 0, "findPetsWithExternalParamRefArraySort method should exist");
+ String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500));
+ Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = ["),
+ "@ValidSort should appear when sort param is resolved from an external $ref parameter");
+ Assert.assertTrue(paramBlock.contains("\"name,asc\"") || paramBlock.contains("\"id,asc\""),
+ "@ValidSort should contain the enum values from the external PetSortEnum schema");
+ Assert.assertTrue(paramBlock.contains("pageable: Pageable"),
+ "findPetsWithExternalParamRefArraySort should have a pageable: Pageable parameter");
+ }
+
+ @Test
+ public void generateSortValidationWorksForNonExplodedExternalParamRefArraySort() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+ additionalProperties.put(GENERATE_SORT_VALIDATION, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ // findPetsWithNonExplodedExternalParamRefArraySort: sort param $ref to external file,
+ // explode: false — Spring parses ?sort=id,asc,name,desc as sequential token pairs.
+ // @ValidSort validation works the same way since it operates on the deserialized Pageable.
+ int methodStart = content.indexOf("fun findPetsWithNonExplodedExternalParamRefArraySort(");
+ Assert.assertTrue(methodStart >= 0, "findPetsWithNonExplodedExternalParamRefArraySort method should exist");
+ String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500));
+ Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = [\"name,asc\", \"name,desc\", \"id,asc\", \"id,desc\"])"),
+ "@ValidSort with PetSortEnum values should appear even for non-exploded array sort param");
+ Assert.assertTrue(paramBlock.contains("pageable: Pageable"),
+ "findPetsWithNonExplodedExternalParamRefArraySort should have a pageable: Pageable parameter");
+ }
+
+ // ========== PAGEABLE DEFAULTS TESTS ==========
+
+ @Test
+ public void pageableDefaultsGeneratesSortDefaultsForSingleDescField() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ assertFileContains(petApi.toPath(),
+ "@SortDefault.SortDefaults(SortDefault(sort = [\"name\"], direction = Sort.Direction.DESC))");
+ assertFileContains(petApi.toPath(), "import org.springframework.data.domain.Sort");
+ assertFileContains(petApi.toPath(), "import org.springframework.data.web.SortDefault");
+ }
+
+ @Test
+ public void pageableDefaultsGeneratesSortDefaultsForSingleAscField() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ assertFileContains(petApi.toPath(),
+ "@SortDefault.SortDefaults(SortDefault(sort = [\"id\"], direction = Sort.Direction.ASC))");
+ }
+
+ @Test
+ public void pageableDefaultsGeneratesSortDefaultsForMixedDirections() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ assertFileContains(petApi.toPath(),
+ "@SortDefault.SortDefaults(SortDefault(sort = [\"name\"], direction = Sort.Direction.DESC), SortDefault(sort = [\"id\"], direction = Sort.Direction.ASC))");
+ }
+
+ @Test
+ public void pageableDefaultsGeneratesPageableDefaultForPageAndSize() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ assertFileContains(petApi.toPath(), "@PageableDefault(page = 0, size = 25)");
+ assertFileContains(petApi.toPath(), "import org.springframework.data.web.PageableDefault");
+ }
+
+ @Test
+ public void pageableDefaultsGeneratesBothAnnotationsWhenAllDefaultsPresent() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ int methodStart = content.indexOf("fun findPetsWithAllDefaults(");
+ Assert.assertTrue(methodStart >= 0, "findPetsWithAllDefaults method should exist");
+ String methodBlock = content.substring(Math.max(0, methodStart - 500), methodStart + 500);
+
+ Assert.assertTrue(methodBlock.contains("@PageableDefault(page = 0, size = 10)"),
+ "findPetsWithAllDefaults should have @PageableDefault(page = 0, size = 10)");
+ Assert.assertTrue(methodBlock.contains(
+ "@SortDefault.SortDefaults(SortDefault(sort = [\"name\"], direction = Sort.Direction.DESC), SortDefault(sort = [\"id\"], direction = Sort.Direction.ASC))"),
+ "findPetsWithAllDefaults should have @SortDefault.SortDefaults with both fields");
+ }
+
+ @Test
+ public void pageableDefaultsDoesNotAnnotateNonPageableOperation() throws Exception {
+ Map additionalProperties = new HashMap<>();
+ additionalProperties.put(USE_TAGS, "true");
+ additionalProperties.put(INTERFACE_ONLY, "true");
+ additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
+
+ Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);
+
+ File petApi = files.get("PetApi.kt");
+ String content = Files.readString(petApi.toPath());
+
+ // findPetsNonPaginatedWithSortEnum has no x-spring-paginated, so no pageable annotations
+ int methodStart = content.indexOf("fun findPetsNonPaginatedWithSortEnum(");
+ Assert.assertTrue(methodStart >= 0, "findPetsNonPaginatedWithSortEnum method should exist");
+ String methodBlock = content.substring(Math.max(0, methodStart - 500), methodStart);
+ Assert.assertFalse(methodBlock.contains("@SortDefault"),
+ "Non-paginated operation should not have @SortDefault");
+ Assert.assertFalse(methodBlock.contains("@PageableDefault"),
+ "Non-paginated operation should not have @PageableDefault");
+ }
+
@Test
public void autoXSpringPaginatedDetectsAllThreeParams() throws Exception {
Map additionalProperties = new HashMap<>();
diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation-components.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation-components.yaml
new file mode 100644
index 000000000000..8f82109468eb
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation-components.yaml
@@ -0,0 +1,31 @@
+components:
+ parameters:
+ PetSortParam:
+ name: sort
+ in: query
+ required: false
+ description: Sort order — multi-column, each value must be one of the allowed enum values
+ schema:
+ type: array
+ default: []
+ items:
+ $ref: '#/components/schemas/PetSortEnum'
+ PetSortParamNonExploded:
+ name: sort
+ in: query
+ required: false
+ style: form
+ explode: false
+ description: Sort order — non-exploded multi-column (e.g. sort=id,asc,name,desc), each token pair must match the allowed enum values
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/PetSortEnum'
+ schemas:
+ PetSortEnum:
+ type: string
+ enum:
+ - "name,asc"
+ - "name,desc"
+ - "id,asc"
+ - "id,desc"
diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation.yaml
new file mode 100644
index 000000000000..86d398d2c407
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation.yaml
@@ -0,0 +1,564 @@
+openapi: 3.0.1
+info:
+ title: OpenAPI Petstore - Sort Validation Test
+ description: Test spec for generateSortValidation feature
+ version: 1.0.0
+servers:
+ - url: http://petstore.swagger.io/v2
+tags:
+ - name: pet
+ description: Everything about your Pets
+paths:
+ /pet/findByStatusWithSort:
+ get:
+ tags:
+ - pet
+ summary: Find pets with explicit x-spring-paginated and inline sort enum
+ operationId: findPetsWithSortEnum
+ x-spring-paginated: true
+ parameters:
+ - name: status
+ in: query
+ description: Status filter
+ schema:
+ type: string
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 20
+ - name: sort
+ in: query
+ description: Sort order
+ schema:
+ type: string
+ enum:
+ - "id,asc"
+ - "id,desc"
+ - "name,asc"
+ - "name,desc"
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findAutoDetectedWithSort:
+ get:
+ tags:
+ - pet
+ summary: Find pets with auto-detected pagination and sort enum
+ operationId: findPetsAutoDetectedWithSort
+ parameters:
+ - name: status
+ in: query
+ description: Status filter
+ schema:
+ type: string
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 20
+ - name: sort
+ in: query
+ description: Sort order
+ schema:
+ type: string
+ enum:
+ - "id,asc"
+ - "id,desc"
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithRefSort:
+ get:
+ tags:
+ - pet
+ summary: Find pets with x-spring-paginated and $ref sort enum
+ operationId: findPetsWithRefSort
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 20
+ - name: sort
+ in: query
+ description: Sort order
+ schema:
+ $ref: '#/components/schemas/PetSort'
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithoutSortEnum:
+ get:
+ tags:
+ - pet
+ summary: Find pets with pagination but sort has no enum constraint
+ operationId: findPetsWithoutSortEnum
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 20
+ - name: sort
+ in: query
+ description: Sort order (no enum constraint)
+ schema:
+ type: string
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findNonPaginatedWithSortEnum:
+ get:
+ tags:
+ - pet
+ summary: Find pets without pagination but sort param has enum — no sort validation expected
+ operationId: findPetsNonPaginatedWithSortEnum
+ parameters:
+ - name: sort
+ in: query
+ description: Sort order with enum but no pagination
+ schema:
+ type: string
+ enum:
+ - "id,asc"
+ - "id,desc"
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+
+ # ---- Pageable defaults test cases ----
+
+ /pet/findWithSortDefaultOnly:
+ get:
+ tags:
+ - pet
+ summary: Find pets — sort default only (single field DESC, no page/size defaults)
+ operationId: findPetsWithSortDefaultOnly
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ - name: size
+ in: query
+ schema:
+ type: integer
+ - name: sort
+ in: query
+ schema:
+ type: string
+ default: "name,desc"
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithSortDefaultAsc:
+ get:
+ tags:
+ - pet
+ summary: Find pets — sort default only (single field, no explicit direction defaults to ASC)
+ operationId: findPetsWithSortDefaultAsc
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ - name: size
+ in: query
+ schema:
+ type: integer
+ - name: sort
+ in: query
+ schema:
+ type: string
+ default: "id"
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithMixedSortDefaults:
+ get:
+ tags:
+ - pet
+ summary: Find pets — multiple sort defaults with mixed directions (array sort param)
+ operationId: findPetsWithMixedSortDefaults
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ - name: size
+ in: query
+ schema:
+ type: integer
+ - name: sort
+ in: query
+ style: form
+ explode: true
+ schema:
+ type: array
+ items:
+ type: string
+ default:
+ - "name,desc"
+ - "id,asc"
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithPageSizeDefaultsOnly:
+ get:
+ tags:
+ - pet
+ summary: Find pets — page and size defaults only, no sort default
+ operationId: findPetsWithPageSizeDefaultsOnly
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 25
+ - name: sort
+ in: query
+ schema:
+ type: string
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithAllDefaults:
+ get:
+ tags:
+ - pet
+ summary: Find pets — page, size, and mixed sort defaults all present
+ operationId: findPetsWithAllDefaults
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 10
+ - name: sort
+ in: query
+ style: form
+ explode: true
+ schema:
+ type: array
+ items:
+ type: string
+ default:
+ - "name,desc"
+ - "id,asc"
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithSizeConstraint:
+ get:
+ tags:
+ - pet
+ summary: Find pets — size has maximum constraint only
+ operationId: findPetsWithSizeConstraint
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ - name: size
+ in: query
+ schema:
+ type: integer
+ maximum: 100
+ - name: sort
+ in: query
+ schema:
+ type: string
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithPageAndSizeConstraint:
+ get:
+ tags:
+ - pet
+ summary: Find pets — both page and size have maximum constraints
+ operationId: findPetsWithPageAndSizeConstraint
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ maximum: 999
+ - name: size
+ in: query
+ schema:
+ type: integer
+ maximum: 50
+ - name: sort
+ in: query
+ schema:
+ type: string
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithArraySortEnum:
+ get:
+ tags:
+ - pet
+ summary: Find pets with x-spring-paginated and array sort param with inline enum on items
+ operationId: findPetsWithArraySortEnum
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 20
+ - name: sort
+ in: query
+ description: Sort order (multi-column, each element must be an allowed value)
+ style: form
+ explode: true
+ schema:
+ type: array
+ items:
+ type: string
+ enum:
+ - "id,asc"
+ - "id,desc"
+ - "name,asc"
+ - "name,desc"
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithArraySortRefEnum:
+ get:
+ tags:
+ - pet
+ summary: Find pets with x-spring-paginated and array sort param whose items use a $ref enum
+ operationId: findPetsWithArraySortRefEnum
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 20
+ - name: sort
+ in: query
+ description: Sort order (multi-column, items $ref to PetSort enum)
+ style: form
+ explode: true
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/PetSort'
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithExternalParamRefArraySort:
+ get:
+ tags:
+ - pet
+ summary: Find pets with x-spring-paginated and sort param referenced from an external components file
+ operationId: findPetsWithExternalParamRefArraySort
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 20
+ - $ref: './petstore-sort-validation-components.yaml#/components/parameters/PetSortParam'
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ /pet/findWithNonExplodedExternalParamRefArraySort:
+ get:
+ tags:
+ - pet
+ summary: Find pets with x-spring-paginated and non-exploded sort param referenced from an external components file
+ operationId: findPetsWithNonExplodedExternalParamRefArraySort
+ x-spring-paginated: true
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: size
+ in: query
+ schema:
+ type: integer
+ default: 20
+ - $ref: './petstore-sort-validation-components.yaml#/components/parameters/PetSortParamNonExploded'
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+components:
+ schemas:
+ PetSort:
+ type: string
+ enum:
+ - "id,asc"
+ - "id,desc"
+ - "createdAt,asc"
+ - "createdAt,desc"
+ Pet:
+ type: object
+ required:
+ - name
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ status:
+ type: string
+ description: pet status in the store
diff --git a/samples/server/petstore/kotlin-spring-default/build.gradle.kts b/samples/server/petstore/kotlin-spring-default/build.gradle.kts
index 319a704f5842..3b564eb0a224 100644
--- a/samples/server/petstore/kotlin-spring-default/build.gradle.kts
+++ b/samples/server/petstore/kotlin-spring-default/build.gradle.kts
@@ -41,6 +41,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-spring-default/pom.xml b/samples/server/petstore/kotlin-spring-default/pom.xml
index 76ef64f955da..3266249b3379 100644
--- a/samples/server/petstore/kotlin-spring-default/pom.xml
+++ b/samples/server/petstore/kotlin-spring-default/pom.xml
@@ -125,6 +125,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-spring-sealed-interfaces/build.gradle.kts b/samples/server/petstore/kotlin-spring-sealed-interfaces/build.gradle.kts
index 559b1f327bec..ff5dc81c6a46 100644
--- a/samples/server/petstore/kotlin-spring-sealed-interfaces/build.gradle.kts
+++ b/samples/server/petstore/kotlin-spring-sealed-interfaces/build.gradle.kts
@@ -37,6 +37,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
diff --git a/samples/server/petstore/kotlin-spring-sealed-interfaces/pom.xml b/samples/server/petstore/kotlin-spring-sealed-interfaces/pom.xml
index 67af2523dcb8..6e01abd57bbb 100644
--- a/samples/server/petstore/kotlin-spring-sealed-interfaces/pom.xml
+++ b/samples/server/petstore/kotlin-spring-sealed-interfaces/pom.xml
@@ -132,6 +132,10 @@
jakarta.validation
jakarta.validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
jakarta.annotation
jakarta.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-3-no-response-entity/build.gradle.kts b/samples/server/petstore/kotlin-springboot-3-no-response-entity/build.gradle.kts
index db73c5e21693..e5e9d4bc3a66 100644
--- a/samples/server/petstore/kotlin-springboot-3-no-response-entity/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-3-no-response-entity/build.gradle.kts
@@ -33,6 +33,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
diff --git a/samples/server/petstore/kotlin-springboot-3-no-response-entity/pom.xml b/samples/server/petstore/kotlin-springboot-3-no-response-entity/pom.xml
index 3844d9e01f44..3af22f009e45 100644
--- a/samples/server/petstore/kotlin-springboot-3-no-response-entity/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-3-no-response-entity/pom.xml
@@ -132,6 +132,10 @@
jakarta.validation
jakarta.validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
jakarta.annotation
jakarta.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-3/build.gradle.kts b/samples/server/petstore/kotlin-springboot-3/build.gradle.kts
index db73c5e21693..e5e9d4bc3a66 100644
--- a/samples/server/petstore/kotlin-springboot-3/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-3/build.gradle.kts
@@ -33,6 +33,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
diff --git a/samples/server/petstore/kotlin-springboot-3/pom.xml b/samples/server/petstore/kotlin-springboot-3/pom.xml
index 3844d9e01f44..3af22f009e45 100644
--- a/samples/server/petstore/kotlin-springboot-3/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-3/pom.xml
@@ -132,6 +132,10 @@
jakarta.validation
jakarta.validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
jakarta.annotation
jakarta.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-4/build.gradle.kts b/samples/server/petstore/kotlin-springboot-4/build.gradle.kts
index 3459856672ac..17e2e5ab29b9 100644
--- a/samples/server/petstore/kotlin-springboot-4/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-4/build.gradle.kts
@@ -33,6 +33,7 @@ dependencies {
implementation("tools.jackson.dataformat:jackson-dataformat-xml")
implementation("tools.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")
diff --git a/samples/server/petstore/kotlin-springboot-4/pom.xml b/samples/server/petstore/kotlin-springboot-4/pom.xml
index 3ce44dbf761c..08ccfa05f02e 100644
--- a/samples/server/petstore/kotlin-springboot-4/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-4/pom.xml
@@ -115,6 +115,10 @@
jakarta.validation
jakarta.validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
jakarta.annotation
jakarta.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-additionalproperties/build.gradle.kts b/samples/server/petstore/kotlin-springboot-additionalproperties/build.gradle.kts
index db73c5e21693..e5e9d4bc3a66 100644
--- a/samples/server/petstore/kotlin-springboot-additionalproperties/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-additionalproperties/build.gradle.kts
@@ -33,6 +33,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
diff --git a/samples/server/petstore/kotlin-springboot-additionalproperties/pom.xml b/samples/server/petstore/kotlin-springboot-additionalproperties/pom.xml
index 3844d9e01f44..3af22f009e45 100644
--- a/samples/server/petstore/kotlin-springboot-additionalproperties/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-additionalproperties/pom.xml
@@ -132,6 +132,10 @@
jakarta.validation
jakarta.validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
jakarta.annotation
jakarta.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-bigdecimal-default/build.gradle.kts b/samples/server/petstore/kotlin-springboot-bigdecimal-default/build.gradle.kts
index 319a704f5842..3b564eb0a224 100644
--- a/samples/server/petstore/kotlin-springboot-bigdecimal-default/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-bigdecimal-default/build.gradle.kts
@@ -41,6 +41,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-bigdecimal-default/pom.xml b/samples/server/petstore/kotlin-springboot-bigdecimal-default/pom.xml
index 76ef64f955da..3266249b3379 100644
--- a/samples/server/petstore/kotlin-springboot-bigdecimal-default/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-bigdecimal-default/pom.xml
@@ -125,6 +125,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-delegate-nodefaults/build.gradle.kts b/samples/server/petstore/kotlin-springboot-delegate-nodefaults/build.gradle.kts
index 6122df330e69..10999697dcaa 100644
--- a/samples/server/petstore/kotlin-springboot-delegate-nodefaults/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-delegate-nodefaults/build.gradle.kts
@@ -34,6 +34,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
diff --git a/samples/server/petstore/kotlin-springboot-delegate-nodefaults/pom.xml b/samples/server/petstore/kotlin-springboot-delegate-nodefaults/pom.xml
index c7a557207ccc..0c987a9af209 100644
--- a/samples/server/petstore/kotlin-springboot-delegate-nodefaults/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-delegate-nodefaults/pom.xml
@@ -138,6 +138,10 @@
jakarta.validation
jakarta.validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
jakarta.annotation
jakarta.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-delegate/build.gradle.kts b/samples/server/petstore/kotlin-springboot-delegate/build.gradle.kts
index 319a704f5842..3b564eb0a224 100644
--- a/samples/server/petstore/kotlin-springboot-delegate/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-delegate/build.gradle.kts
@@ -41,6 +41,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-delegate/pom.xml b/samples/server/petstore/kotlin-springboot-delegate/pom.xml
index 76ef64f955da..3266249b3379 100644
--- a/samples/server/petstore/kotlin-springboot-delegate/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-delegate/pom.xml
@@ -125,6 +125,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/build.gradle.kts b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/build.gradle.kts
index 3fa8025c39dd..0f622040f5d6 100644
--- a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/build.gradle.kts
@@ -44,6 +44,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/pom.xml b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/pom.xml
index 9e5b5f9d5877..a8e82b9771f9 100644
--- a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/pom.xml
@@ -136,6 +136,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt
index 977ec3a70961..ee67a71a0410 100644
--- a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt
+++ b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt
@@ -7,6 +7,7 @@ package org.openapitools.api
import org.openapitools.model.ModelApiResponse
import org.springframework.data.domain.Pageable
+import org.springframework.data.web.PageableDefault
import org.openapitools.model.Pet
import io.swagger.annotations.Api
import io.swagger.annotations.ApiOperation
@@ -183,7 +184,7 @@ interface PetApi {
produces = ["application/json"]
)
fun listAllPetsPaginated(@ApiParam(hidden = true) exchange: org.springframework.web.server.ServerWebExchange,
- @ApiParam(hidden = true) pageable: Pageable): ResponseEntity> {
+ @PageableDefault(page = 0, size = 20) @ApiParam(hidden = true) pageable: Pageable): ResponseEntity> {
return getDelegate().listAllPetsPaginated(exchange, pageable)
}
diff --git a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt
index f1b877a0fbb7..ad4642030058 100644
--- a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt
+++ b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt
@@ -2,6 +2,7 @@ package org.openapitools.api
import org.openapitools.model.ModelApiResponse
import org.springframework.data.domain.Pageable
+import org.springframework.data.web.PageableDefault
import org.openapitools.model.Pet
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
diff --git a/samples/server/petstore/kotlin-springboot-integer-enum/build.gradle.kts b/samples/server/petstore/kotlin-springboot-integer-enum/build.gradle.kts
index 559b1f327bec..ff5dc81c6a46 100644
--- a/samples/server/petstore/kotlin-springboot-integer-enum/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-integer-enum/build.gradle.kts
@@ -37,6 +37,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
diff --git a/samples/server/petstore/kotlin-springboot-integer-enum/pom.xml b/samples/server/petstore/kotlin-springboot-integer-enum/pom.xml
index 67af2523dcb8..6e01abd57bbb 100644
--- a/samples/server/petstore/kotlin-springboot-integer-enum/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-integer-enum/pom.xml
@@ -132,6 +132,10 @@
jakarta.validation
jakarta.validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
jakarta.annotation
jakarta.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-modelMutable/build.gradle.kts b/samples/server/petstore/kotlin-springboot-modelMutable/build.gradle.kts
index 319a704f5842..3b564eb0a224 100644
--- a/samples/server/petstore/kotlin-springboot-modelMutable/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-modelMutable/build.gradle.kts
@@ -41,6 +41,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-modelMutable/pom.xml b/samples/server/petstore/kotlin-springboot-modelMutable/pom.xml
index 76ef64f955da..3266249b3379 100644
--- a/samples/server/petstore/kotlin-springboot-modelMutable/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-modelMutable/pom.xml
@@ -125,6 +125,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-multipart-request-model/build.gradle.kts b/samples/server/petstore/kotlin-springboot-multipart-request-model/build.gradle.kts
index 319a704f5842..3b564eb0a224 100644
--- a/samples/server/petstore/kotlin-springboot-multipart-request-model/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-multipart-request-model/build.gradle.kts
@@ -41,6 +41,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-multipart-request-model/pom.xml b/samples/server/petstore/kotlin-springboot-multipart-request-model/pom.xml
index 76ef64f955da..3266249b3379 100644
--- a/samples/server/petstore/kotlin-springboot-multipart-request-model/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-multipart-request-model/pom.xml
@@ -125,6 +125,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/build.gradle.kts b/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/build.gradle.kts
index b3086b53f094..7dcfd792a49e 100644
--- a/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/pom.xml b/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/pom.xml
index 7a6726d7bbfd..37342a0b679b 100644
--- a/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/pom.xml
@@ -119,6 +119,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-no-response-entity/build.gradle.kts b/samples/server/petstore/kotlin-springboot-no-response-entity/build.gradle.kts
index b3086b53f094..7dcfd792a49e 100644
--- a/samples/server/petstore/kotlin-springboot-no-response-entity/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-no-response-entity/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-no-response-entity/pom.xml b/samples/server/petstore/kotlin-springboot-no-response-entity/pom.xml
index 7a6726d7bbfd..37342a0b679b 100644
--- a/samples/server/petstore/kotlin-springboot-no-response-entity/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-no-response-entity/pom.xml
@@ -119,6 +119,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-reactive-without-flow/build.gradle.kts b/samples/server/petstore/kotlin-springboot-reactive-without-flow/build.gradle.kts
index f8d51547a12e..9294e90a715a 100644
--- a/samples/server/petstore/kotlin-springboot-reactive-without-flow/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-reactive-without-flow/build.gradle.kts
@@ -44,6 +44,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-reactive-without-flow/pom.xml b/samples/server/petstore/kotlin-springboot-reactive-without-flow/pom.xml
index 39afc668db31..d0e993868ac5 100644
--- a/samples/server/petstore/kotlin-springboot-reactive-without-flow/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-reactive-without-flow/pom.xml
@@ -136,6 +136,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-reactive/build.gradle.kts b/samples/server/petstore/kotlin-springboot-reactive/build.gradle.kts
index f8d51547a12e..9294e90a715a 100644
--- a/samples/server/petstore/kotlin-springboot-reactive/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-reactive/build.gradle.kts
@@ -44,6 +44,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-reactive/pom.xml b/samples/server/petstore/kotlin-springboot-reactive/pom.xml
index 39afc668db31..d0e993868ac5 100644
--- a/samples/server/petstore/kotlin-springboot-reactive/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-reactive/pom.xml
@@ -136,6 +136,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-request-cookie/build.gradle.kts b/samples/server/petstore/kotlin-springboot-request-cookie/build.gradle.kts
index fa644c9b144c..ddfd0a5cffd6 100644
--- a/samples/server/petstore/kotlin-springboot-request-cookie/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-request-cookie/build.gradle.kts
@@ -38,6 +38,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
diff --git a/samples/server/petstore/kotlin-springboot-request-cookie/pom.xml b/samples/server/petstore/kotlin-springboot-request-cookie/pom.xml
index 797865c697a9..c7a7145785d9 100644
--- a/samples/server/petstore/kotlin-springboot-request-cookie/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-request-cookie/pom.xml
@@ -138,6 +138,10 @@
jakarta.validation
jakarta.validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
jakarta.annotation
jakarta.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator-ignore b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator-ignore
new file mode 100644
index 000000000000..7484ee590a38
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/FILES b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/FILES
new file mode 100644
index 000000000000..90a2baceec60
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/FILES
@@ -0,0 +1,17 @@
+README.md
+build.gradle.kts
+gradle/wrapper/gradle-wrapper.jar
+gradle/wrapper/gradle-wrapper.properties
+gradlew
+gradlew.bat
+pom.xml
+settings.gradle
+src/main/kotlin/org/openapitools/api/ApiUtil.kt
+src/main/kotlin/org/openapitools/api/Exceptions.kt
+src/main/kotlin/org/openapitools/api/PetApi.kt
+src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
+src/main/kotlin/org/openapitools/configuration/ValidPageable.kt
+src/main/kotlin/org/openapitools/configuration/ValidSort.kt
+src/main/kotlin/org/openapitools/model/Pet.kt
+src/main/kotlin/org/openapitools/model/PetSort.kt
+src/main/kotlin/org/openapitools/model/PetSortEnum.kt
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/VERSION b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/VERSION
new file mode 100644
index 000000000000..f7962df3e243
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.22.0-SNAPSHOT
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/README.md b/samples/server/petstore/kotlin-springboot-sort-validation/README.md
new file mode 100644
index 000000000000..3808563e513f
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/README.md
@@ -0,0 +1,21 @@
+# openAPIPetstoreSortValidationTest
+
+This Kotlin based [Spring Boot](https://spring.io/projects/spring-boot) application has been generated using the [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator).
+
+## Getting Started
+
+This document assumes you have either maven or gradle available, either via the wrapper or otherwise. This does not come with a gradle / maven wrapper checked in.
+
+By default a [`pom.xml`](pom.xml) file will be generated. If you specified `gradleBuildFile=true` when generating this project, a `build.gradle.kts` will also be generated. Note this uses [Gradle Kotlin DSL](https://github.com/gradle/kotlin-dsl).
+
+To build the project using maven, run:
+```bash
+mvn package && java -jar target/openapi-spring-1.0.0.jar
+```
+
+To build the project using gradle, run:
+```bash
+gradle build && java -jar build/libs/openapi-spring-1.0.0.jar
+```
+
+If all builds successfully, the server should run on [http://localhost:8080/](http://localhost:8080/)
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/build.gradle.kts b/samples/server/petstore/kotlin-springboot-sort-validation/build.gradle.kts
new file mode 100644
index 000000000000..ff5dc81c6a46
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/build.gradle.kts
@@ -0,0 +1,48 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+group = "org.openapitools"
+version = "1.0.0"
+java.sourceCompatibility = JavaVersion.VERSION_17
+
+repositories {
+ mavenCentral()
+ maven { url = uri("https://repo.spring.io/milestone") }
+}
+
+tasks.withType {
+ kotlinOptions.jvmTarget = "17"
+}
+
+tasks.bootJar {
+ enabled = false
+}
+
+plugins {
+ val kotlinVersion = "1.9.25"
+ id("org.jetbrains.kotlin.jvm") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
+ id("org.springframework.boot") version "3.0.2"
+ id("io.spring.dependency-management") version "1.0.14.RELEASE"
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+ implementation("org.jetbrains.kotlin:kotlin-reflect")
+ implementation("org.springframework.boot:spring-boot-starter-web")
+
+ implementation("com.google.code.findbugs:jsr305:3.0.2")
+ implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
+ implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+ implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
+ implementation("jakarta.validation:jakarta.validation-api")
+ implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
+
+ testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
+ testImplementation("org.springframework.boot:spring-boot-starter-test") {
+ exclude(module = "junit")
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.jar b/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000000..e6441136f3d4
Binary files /dev/null and b/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.properties b/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000000..80187ac30432
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/gradlew b/samples/server/petstore/kotlin-springboot-sort-validation/gradlew
new file mode 100644
index 000000000000..9d0ce634cb11
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+[ -h "$app_path" ]
+do
+ls=$( ls -ld "$app_path" )
+link=${ls#*' -> '}
+case $link in #(
+/*) app_path=$link ;; #(
+*) app_path=$APP_HOME$link ;;
+esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+echo "$*"
+} >&2
+
+die () {
+echo
+echo "$*"
+echo
+exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+CYGWIN* ) cygwin=true ;; #(
+Darwin* ) darwin=true ;; #(
+MSYS* | MINGW* ) msys=true ;; #(
+NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+# IBM's JDK on AIX uses strange locations for the executables
+JAVACMD=$JAVA_HOME/jre/sh/java
+else
+JAVACMD=$JAVA_HOME/bin/java
+fi
+if [ ! -x "$JAVACMD" ] ; then
+die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+else
+JAVACMD=java
+if ! command -v java >/dev/null 2>&1
+then
+die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+case $MAX_FD in #(
+max*)
+# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+# shellcheck disable=SC2039,SC3045
+MAX_FD=$( ulimit -H -n ) ||
+warn "Could not query maximum file descriptor limit"
+esac
+case $MAX_FD in #(
+'' | soft) :;; #(
+*)
+# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+# shellcheck disable=SC2039,SC3045
+ulimit -n "$MAX_FD" ||
+warn "Could not set maximum file descriptor limit to $MAX_FD"
+esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+# Now convert the arguments - kludge to limit ourselves to /bin/sh
+for arg do
+if
+case $arg in #(
+-*) false ;; # don't mess with options #(
+/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+[ -e "$t" ] ;; #(
+*) false ;;
+esac
+then
+arg=$( cygpath --path --ignore --mixed "$arg" )
+fi
+# Roll the args list around exactly as many times as the number of
+# args, so each arg winds up back in the position where it started, but
+# possibly modified.
+#
+# NB: a `for` loop captures its iteration list before it begins, so
+# changing the positional parameters here affects neither the number of
+# iterations, nor the values presented in `arg`.
+shift # remove old arg
+set -- "$@" "$arg" # push replacement arg
+done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+"-Dorg.gradle.appname=$APP_BASE_NAME" \
+-classpath "$CLASSPATH" \
+org.gradle.wrapper.GradleWrapperMain \
+"$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+xargs -n1 |
+sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+tr '\n' ' '
+)" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/gradlew.bat b/samples/server/petstore/kotlin-springboot-sort-validation/gradlew.bat
new file mode 100644
index 000000000000..25da30dbdeee
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/pom.xml b/samples/server/petstore/kotlin-springboot-sort-validation/pom.xml
new file mode 100644
index 000000000000..6e01abd57bbb
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/pom.xml
@@ -0,0 +1,152 @@
+
+ 4.0.0
+ org.openapitools
+ openapi-spring
+ jar
+ openapi-spring
+ 1.0.0
+
+ 3.0.2
+ 2.1.0
+ 1.7.10
+
+ 1.7.10
+ UTF-8
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.3
+
+
+
+ repository.spring.milestone
+ Spring Milestone Repository
+ https://repo.spring.io/milestone
+
+
+
+
+ spring-milestones
+ https://repo.spring.io/milestone
+
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/test/kotlin
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+ kotlin-maven-plugin
+ org.jetbrains.kotlin
+ ${kotlin.version}
+
+
+ spring
+
+ 17
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ ${kotlin.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.data
+ spring-data-commons
+
+
+
+
+
+
+ com.google.code.findbugs
+ jsr305
+ ${findbugs-jsr305.version}
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta-annotation.version}
+ provided
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ ${kotlin-test-junit5.version}
+ test
+
+
+
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/settings.gradle b/samples/server/petstore/kotlin-springboot-sort-validation/settings.gradle
new file mode 100644
index 000000000000..14844905cd40
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/settings.gradle
@@ -0,0 +1,15 @@
+pluginManagement {
+ repositories {
+ maven { url = uri("https://repo.spring.io/snapshot") }
+ maven { url = uri("https://repo.spring.io/milestone") }
+ gradlePluginPortal()
+ }
+ resolutionStrategy {
+ eachPlugin {
+ if (requested.id.id == "org.springframework.boot") {
+ useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}")
+ }
+ }
+ }
+}
+rootProject.name = "openapi-spring"
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/Application.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/Application.kt
new file mode 100644
index 000000000000..411871b327af
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/Application.kt
@@ -0,0 +1,13 @@
+package org.openapitools
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.ComponentScan
+
+@SpringBootApplication
+@ComponentScan("org.openapitools")
+class Application
+
+fun main(args: Array) {
+ runApplication(*args)
+}
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/ApiUtil.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/ApiUtil.kt
new file mode 100644
index 000000000000..03344e13b474
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/ApiUtil.kt
@@ -0,0 +1,19 @@
+package org.openapitools.api
+
+import org.springframework.web.context.request.NativeWebRequest
+
+import jakarta.servlet.http.HttpServletResponse
+import java.io.IOException
+
+object ApiUtil {
+ fun setExampleResponse(req: NativeWebRequest, contentType: String, example: String) {
+ try {
+ val res = req.getNativeResponse(HttpServletResponse::class.java)
+ res?.characterEncoding = "UTF-8"
+ res?.addHeader("Content-Type", contentType)
+ res?.writer?.print(example)
+ } catch (e: IOException) {
+ throw RuntimeException(e)
+ }
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/Exceptions.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/Exceptions.kt
new file mode 100644
index 000000000000..1bd78f54576a
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/Exceptions.kt
@@ -0,0 +1,30 @@
+package org.openapitools.api
+
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpStatus
+import org.springframework.web.bind.annotation.ControllerAdvice
+import org.springframework.web.bind.annotation.ExceptionHandler
+import jakarta.servlet.http.HttpServletResponse
+import jakarta.validation.ConstraintViolationException
+
+// TODO Extend ApiException for custom exception handling, e.g. the below NotFound exception
+sealed class ApiException(msg: String, val code: Int) : Exception(msg)
+
+class NotFoundException(msg: String, code: Int = HttpStatus.NOT_FOUND.value()) : ApiException(msg, code)
+
+@Configuration("org.openapitools.api.DefaultExceptionHandler")
+@ControllerAdvice
+class DefaultExceptionHandler {
+
+ @ExceptionHandler(value = [ApiException::class])
+ fun onApiException(ex: ApiException, response: HttpServletResponse): Unit =
+ response.sendError(ex.code, ex.message)
+
+ @ExceptionHandler(value = [NotImplementedError::class])
+ fun onNotImplemented(ex: NotImplementedError, response: HttpServletResponse): Unit =
+ response.sendError(HttpStatus.NOT_IMPLEMENTED.value())
+
+ @ExceptionHandler(value = [ConstraintViolationException::class])
+ fun onConstraintViolation(ex: ConstraintViolationException, response: HttpServletResponse): Unit =
+ response.sendError(HttpStatus.BAD_REQUEST.value(), ex.constraintViolations.joinToString(", ") { it.message })
+}
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApi.kt
new file mode 100644
index 000000000000..ab39b580d32d
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApi.kt
@@ -0,0 +1,250 @@
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.22.0-SNAPSHOT).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+*/
+package org.openapitools.api
+
+import org.springframework.data.domain.Pageable
+import org.springframework.data.web.PageableDefault
+import org.openapitools.model.Pet
+import org.openapitools.model.PetSort
+import org.openapitools.model.PetSortEnum
+import org.springframework.data.domain.Sort
+import org.springframework.data.web.SortDefault
+import org.openapitools.configuration.ValidPageable
+import org.openapitools.configuration.ValidSort
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.http.ResponseEntity
+
+import org.springframework.web.bind.annotation.*
+import org.springframework.validation.annotation.Validated
+import org.springframework.web.context.request.NativeWebRequest
+import org.springframework.beans.factory.annotation.Autowired
+
+import jakarta.validation.constraints.DecimalMax
+import jakarta.validation.constraints.DecimalMin
+import jakarta.validation.constraints.Email
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.NotNull
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.Size
+import jakarta.validation.Valid
+
+import kotlin.collections.List
+import kotlin.collections.Map
+
+@RestController
+@Validated
+@RequestMapping("\${api.base-path:/v2}")
+interface PetApi {
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findAutoDetectedWithSort"
+ value = [PATH_FIND_PETS_AUTO_DETECTED_WITH_SORT],
+ produces = ["application/json"]
+ )
+ fun findPetsAutoDetectedWithSort(
+ @Valid @RequestParam(value = "status", required = false) status: kotlin.String?,
+ @Valid @RequestParam(value = "page", required = false, defaultValue = "0") page: kotlin.Int,
+ @Valid @RequestParam(value = "size", required = false, defaultValue = "20") size: kotlin.Int,
+ @Valid @RequestParam(value = "sort", required = false) sort: kotlin.String?
+ ): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findNonPaginatedWithSortEnum"
+ value = [PATH_FIND_PETS_NON_PAGINATED_WITH_SORT_ENUM],
+ produces = ["application/json"]
+ )
+ fun findPetsNonPaginatedWithSortEnum(
+ @Valid @RequestParam(value = "sort", required = false) sort: kotlin.String?
+ ): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithAllDefaults"
+ value = [PATH_FIND_PETS_WITH_ALL_DEFAULTS],
+ produces = ["application/json"]
+ )
+ fun findPetsWithAllDefaults(@PageableDefault(page = 0, size = 10) @SortDefault.SortDefaults(SortDefault(sort = ["name"], direction = Sort.Direction.DESC), SortDefault(sort = ["id"], direction = Sort.Direction.ASC)) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithArraySortEnum"
+ value = [PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM],
+ produces = ["application/json"]
+ )
+ fun findPetsWithArraySortEnum(@ValidSort(allowedValues = ["id,asc", "id,desc", "name,asc", "name,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithArraySortRefEnum"
+ value = [PATH_FIND_PETS_WITH_ARRAY_SORT_REF_ENUM],
+ produces = ["application/json"]
+ )
+ fun findPetsWithArraySortRefEnum(@ValidSort(allowedValues = ["id,asc", "id,desc", "createdAt,asc", "createdAt,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithExternalParamRefArraySort"
+ value = [PATH_FIND_PETS_WITH_EXTERNAL_PARAM_REF_ARRAY_SORT],
+ produces = ["application/json"]
+ )
+ fun findPetsWithExternalParamRefArraySort(@ValidSort(allowedValues = ["name,asc", "name,desc", "id,asc", "id,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithMixedSortDefaults"
+ value = [PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS],
+ produces = ["application/json"]
+ )
+ fun findPetsWithMixedSortDefaults(@SortDefault.SortDefaults(SortDefault(sort = ["name"], direction = Sort.Direction.DESC), SortDefault(sort = ["id"], direction = Sort.Direction.ASC)) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithNonExplodedExternalParamRefArraySort"
+ value = [PATH_FIND_PETS_WITH_NON_EXPLODED_EXTERNAL_PARAM_REF_ARRAY_SORT],
+ produces = ["application/json"]
+ )
+ fun findPetsWithNonExplodedExternalParamRefArraySort(@ValidSort(allowedValues = ["name,asc", "name,desc", "id,asc", "id,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithPageAndSizeConstraint"
+ value = [PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT],
+ produces = ["application/json"]
+ )
+ fun findPetsWithPageAndSizeConstraint(@ValidPageable(maxSize = 50, maxPage = 999) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithPageSizeDefaultsOnly"
+ value = [PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY],
+ produces = ["application/json"]
+ )
+ fun findPetsWithPageSizeDefaultsOnly(@PageableDefault(page = 0, size = 25) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithRefSort"
+ value = [PATH_FIND_PETS_WITH_REF_SORT],
+ produces = ["application/json"]
+ )
+ fun findPetsWithRefSort(@ValidSort(allowedValues = ["id,asc", "id,desc", "createdAt,asc", "createdAt,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithSizeConstraint"
+ value = [PATH_FIND_PETS_WITH_SIZE_CONSTRAINT],
+ produces = ["application/json"]
+ )
+ fun findPetsWithSizeConstraint(@ValidPageable(maxSize = 100) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithSortDefaultAsc"
+ value = [PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC],
+ produces = ["application/json"]
+ )
+ fun findPetsWithSortDefaultAsc(@SortDefault.SortDefaults(SortDefault(sort = ["id"], direction = Sort.Direction.ASC)) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithSortDefaultOnly"
+ value = [PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY],
+ produces = ["application/json"]
+ )
+ fun findPetsWithSortDefaultOnly(@SortDefault.SortDefaults(SortDefault(sort = ["name"], direction = Sort.Direction.DESC)) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findByStatusWithSort"
+ value = [PATH_FIND_PETS_WITH_SORT_ENUM],
+ produces = ["application/json"]
+ )
+ fun findPetsWithSortEnum(
+ @Valid @RequestParam(value = "status", required = false) status: kotlin.String?,
+ @ValidSort(allowedValues = ["id,asc", "id,desc", "name,asc", "name,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable
+ ): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findWithoutSortEnum"
+ value = [PATH_FIND_PETS_WITHOUT_SORT_ENUM],
+ produces = ["application/json"]
+ )
+ fun findPetsWithoutSortEnum(@PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> {
+ return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
+ }
+
+ companion object {
+ //for your own safety never directly reuse these path definitions in tests
+ const val BASE_PATH: String = "/v2"
+ const val PATH_FIND_PETS_AUTO_DETECTED_WITH_SORT: String = "/pet/findAutoDetectedWithSort"
+ const val PATH_FIND_PETS_NON_PAGINATED_WITH_SORT_ENUM: String = "/pet/findNonPaginatedWithSortEnum"
+ const val PATH_FIND_PETS_WITH_ALL_DEFAULTS: String = "/pet/findWithAllDefaults"
+ const val PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM: String = "/pet/findWithArraySortEnum"
+ const val PATH_FIND_PETS_WITH_ARRAY_SORT_REF_ENUM: String = "/pet/findWithArraySortRefEnum"
+ const val PATH_FIND_PETS_WITH_EXTERNAL_PARAM_REF_ARRAY_SORT: String = "/pet/findWithExternalParamRefArraySort"
+ const val PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS: String = "/pet/findWithMixedSortDefaults"
+ const val PATH_FIND_PETS_WITH_NON_EXPLODED_EXTERNAL_PARAM_REF_ARRAY_SORT: String = "/pet/findWithNonExplodedExternalParamRefArraySort"
+ const val PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT: String = "/pet/findWithPageAndSizeConstraint"
+ const val PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY: String = "/pet/findWithPageSizeDefaultsOnly"
+ const val PATH_FIND_PETS_WITH_REF_SORT: String = "/pet/findWithRefSort"
+ const val PATH_FIND_PETS_WITH_SIZE_CONSTRAINT: String = "/pet/findWithSizeConstraint"
+ const val PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC: String = "/pet/findWithSortDefaultAsc"
+ const val PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY: String = "/pet/findWithSortDefaultOnly"
+ const val PATH_FIND_PETS_WITH_SORT_ENUM: String = "/pet/findByStatusWithSort"
+ const val PATH_FIND_PETS_WITHOUT_SORT_ENUM: String = "/pet/findWithoutSortEnum"
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApiImpl.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApiImpl.kt
new file mode 100644
index 000000000000..6e7ee15a7a3d
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApiImpl.kt
@@ -0,0 +1,132 @@
+package org.openapitools.api
+
+import org.openapitools.model.Pet
+import org.springframework.data.domain.Pageable
+import org.springframework.data.domain.Sort
+import org.springframework.http.ResponseEntity
+import org.springframework.stereotype.Component
+import org.springframework.web.bind.annotation.RestController
+
+/**
+ * Sample implementation of [PetApi] demonstrating that the generated
+ * annotations (@ValidSort, @ValidPageable, @PageableDefault, @SortDefault)
+ * behave correctly at runtime.
+ *
+ * Methods whose endpoint carries pagination/sort defaults assert the exact
+ * expected values inside the method body. When the Spring argument resolvers
+ * apply the annotated defaults correctly, the assertions pass and HTTP 200 is
+ * returned. If a default is missing or wrong, the assertion throws
+ * [IllegalStateException] and the request fails with HTTP 500, which causes
+ * any calling test to fail with a clear message.
+ *
+ * Methods that only carry @ValidSort / @ValidPageable constraints need no body
+ * logic — the constraint annotations reject invalid input before this code is
+ * ever reached, and the [DefaultExceptionHandler] maps the resulting
+ * [jakarta.validation.ConstraintViolationException] to HTTP 400.
+ */
+@RestController
+class PetApiImpl : PetApi {
+
+ // ── no pageable / no special defaults ────────────────────────────────────
+
+ override fun findPetsAutoDetectedWithSort(
+ status: String?,
+ page: Int,
+ size: Int,
+ sort: String?,
+ ): ResponseEntity> = ResponseEntity.ok(emptyList())
+
+ override fun findPetsNonPaginatedWithSortEnum(
+ sort: String?,
+ ): ResponseEntity> = ResponseEntity.ok(emptyList())
+
+ // ── @ValidSort only (+ @PageableDefault) ─────────────────────────────────
+ // Validation rejects bad sort values before the method is called.
+
+ override fun findPetsWithArraySortEnum(pageable: Pageable): ResponseEntity> =
+ ResponseEntity.ok(emptyList())
+
+ override fun findPetsWithArraySortRefEnum(pageable: Pageable): ResponseEntity> =
+ ResponseEntity.ok(emptyList())
+
+ override fun findPetsWithExternalParamRefArraySort(pageable: Pageable): ResponseEntity> =
+ ResponseEntity.ok(emptyList())
+
+ override fun findPetsWithNonExplodedExternalParamRefArraySort(pageable: Pageable): ResponseEntity> =
+ ResponseEntity.ok(emptyList())
+
+ override fun findPetsWithRefSort(pageable: Pageable): ResponseEntity> =
+ ResponseEntity.ok(emptyList())
+
+ override fun findPetsWithSortEnum(status: String?, pageable: Pageable): ResponseEntity> =
+ ResponseEntity.ok(emptyList())
+
+ override fun findPetsWithoutSortEnum(pageable: Pageable): ResponseEntity> =
+ ResponseEntity.ok(emptyList())
+
+ // ── @ValidPageable only ───────────────────────────────────────────────────
+ // Validation rejects out-of-range page / size before the method is called.
+
+ override fun findPetsWithSizeConstraint(pageable: Pageable): ResponseEntity> =
+ ResponseEntity.ok(emptyList())
+
+ override fun findPetsWithPageAndSizeConstraint(pageable: Pageable): ResponseEntity> =
+ ResponseEntity.ok(emptyList())
+
+ // ── @PageableDefault ─────────────────────────────────────────────────────
+ // @PageableDefault(page = 0, size = 25)
+
+ override fun findPetsWithPageSizeDefaultsOnly(pageable: Pageable): ResponseEntity> {
+ check(pageable.pageNumber == 0) { "@PageableDefault page: expected 0, got ${pageable.pageNumber}" }
+ check(pageable.pageSize == 25) { "@PageableDefault size: expected 25, got ${pageable.pageSize}" }
+ return ResponseEntity.ok(emptyList())
+ }
+
+ // ── @SortDefault ─────────────────────────────────────────────────────────
+ // @SortDefault(sort = ["name"], direction = DESC)
+
+ override fun findPetsWithSortDefaultOnly(pageable: Pageable): ResponseEntity> {
+ check(pageable.sort.getOrderFor("name")?.direction == Sort.Direction.DESC) {
+ "@SortDefault sort: expected name DESC, got ${pageable.sort}"
+ }
+ return ResponseEntity.ok(emptyList())
+ }
+
+ // @SortDefault(sort = ["id"], direction = ASC)
+
+ override fun findPetsWithSortDefaultAsc(pageable: Pageable): ResponseEntity> {
+ check(pageable.sort.getOrderFor("id")?.direction == Sort.Direction.ASC) {
+ "@SortDefault sort: expected id ASC, got ${pageable.sort}"
+ }
+ return ResponseEntity.ok(emptyList())
+ }
+
+ // ── @SortDefault.SortDefaults ─────────────────────────────────────────────
+ // @SortDefaults(SortDefault(sort = ["name"], direction = DESC), SortDefault(sort = ["id"], direction = ASC))
+
+ override fun findPetsWithMixedSortDefaults(pageable: Pageable): ResponseEntity> {
+ check(pageable.sort.getOrderFor("name")?.direction == Sort.Direction.DESC) {
+ "@SortDefaults sort: expected name DESC, got ${pageable.sort}"
+ }
+ check(pageable.sort.getOrderFor("id")?.direction == Sort.Direction.ASC) {
+ "@SortDefaults sort: expected id ASC, got ${pageable.sort}"
+ }
+ return ResponseEntity.ok(emptyList())
+ }
+
+ // ── @PageableDefault + @SortDefault.SortDefaults combined ─────────────────
+ // @PageableDefault(page = 0, size = 10)
+ // @SortDefaults(SortDefault(sort = ["name"], direction = DESC), SortDefault(sort = ["id"], direction = ASC))
+
+ override fun findPetsWithAllDefaults(pageable: Pageable): ResponseEntity> {
+ check(pageable.pageNumber == 0) { "@PageableDefault page: expected 0, got ${pageable.pageNumber}" }
+ check(pageable.pageSize == 10) { "@PageableDefault size: expected 10, got ${pageable.pageSize}" }
+ check(pageable.sort.getOrderFor("name")?.direction == Sort.Direction.DESC) {
+ "@SortDefaults sort: expected name DESC, got ${pageable.sort}"
+ }
+ check(pageable.sort.getOrderFor("id")?.direction == Sort.Direction.ASC) {
+ "@SortDefaults sort: expected id ASC, got ${pageable.sort}"
+ }
+ return ResponseEntity.ok(emptyList())
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
new file mode 100644
index 000000000000..131f87bc8c74
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
@@ -0,0 +1,33 @@
+package org.openapitools.configuration
+
+import org.openapitools.model.PetSort
+import org.openapitools.model.PetSortEnum
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.convert.converter.Converter
+
+/**
+ * This class provides Spring Converter beans for the enum models in the OpenAPI specification.
+ *
+ * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent
+ * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than
+ * `original` or the specification has an integer enum.
+ */
+@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration")
+class EnumConverterConfiguration {
+
+ @Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.petSortConverter"])
+ fun petSortConverter(): Converter {
+ return object: Converter {
+ override fun convert(source: kotlin.String): PetSort = PetSort.forValue(source)
+ }
+ }
+ @Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.petSortEnumConverter"])
+ fun petSortEnumConverter(): Converter {
+ return object: Converter {
+ override fun convert(source: kotlin.String): PetSortEnum = PetSortEnum.forValue(source)
+ }
+ }
+
+}
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidPageable.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidPageable.kt
new file mode 100644
index 000000000000..671e682ec6fe
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidPageable.kt
@@ -0,0 +1,81 @@
+package org.openapitools.configuration
+
+import jakarta.validation.Constraint
+import jakarta.validation.ConstraintValidator
+import jakarta.validation.ConstraintValidatorContext
+import jakarta.validation.Payload
+import org.springframework.data.domain.Pageable
+
+/**
+ * Validates that the page number and page size in the annotated [Pageable] parameter do not
+ * exceed their configured maximums.
+ *
+ * Apply directly on a `pageable: Pageable` parameter. Each attribute is independently optional:
+ * - [maxSize] — when set (>= 0), validates `pageable.pageSize <= maxSize`
+ * - [maxPage] — when set (>= 0), validates `pageable.pageNumber <= maxPage`
+ *
+ * Use [NO_LIMIT] (= -1, the default) to leave an attribute unconstrained.
+ *
+ * Constraining [maxPage] is useful to prevent deep-pagination attacks, where a large page
+ * offset (e.g. `?page=100000&size=20`) causes an expensive `OFFSET` query on the database.
+ *
+ * @property maxSize Maximum allowed page size, or [NO_LIMIT] if unconstrained
+ * @property maxPage Maximum allowed page number (0-based), or [NO_LIMIT] if unconstrained
+ * @property groups Validation groups (optional)
+ * @property payload Additional payload (optional)
+ * @property message Validation error message (default: "Invalid page request")
+ */
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+@Constraint(validatedBy = [PageableConstraintValidator::class])
+@Target(AnnotationTarget.VALUE_PARAMETER)
+annotation class ValidPageable(
+ val maxSize: Int = ValidPageable.NO_LIMIT,
+ val maxPage: Int = ValidPageable.NO_LIMIT,
+ val groups: Array> = [],
+ val payload: Array> = [],
+ val message: String = "Invalid page request"
+) {
+ companion object {
+ const val NO_LIMIT = -1
+ }
+}
+
+class PageableConstraintValidator : ConstraintValidator {
+
+ private var maxSize = ValidPageable.NO_LIMIT
+ private var maxPage = ValidPageable.NO_LIMIT
+
+ override fun initialize(constraintAnnotation: ValidPageable) {
+ maxSize = constraintAnnotation.maxSize
+ maxPage = constraintAnnotation.maxPage
+ }
+
+ override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean {
+ if (pageable == null) return true
+ if (!pageable.isPaged) return true
+
+ var valid = true
+ context.disableDefaultConstraintViolation()
+
+ if (maxSize >= 0 && pageable.pageSize > maxSize) {
+ context.buildConstraintViolationWithTemplate(
+ "${context.defaultConstraintMessageTemplate}: page size ${pageable.pageSize} exceeds maximum $maxSize"
+ )
+ .addPropertyNode("size")
+ .addConstraintViolation()
+ valid = false
+ }
+
+ if (maxPage >= 0 && pageable.pageNumber > maxPage) {
+ context.buildConstraintViolationWithTemplate(
+ "${context.defaultConstraintMessageTemplate}: page number ${pageable.pageNumber} exceeds maximum $maxPage"
+ )
+ .addPropertyNode("page")
+ .addConstraintViolation()
+ valid = false
+ }
+
+ return valid
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidSort.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidSort.kt
new file mode 100644
index 000000000000..5c96f1c82814
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidSort.kt
@@ -0,0 +1,87 @@
+package org.openapitools.configuration
+
+import jakarta.validation.Constraint
+import jakarta.validation.ConstraintValidator
+import jakarta.validation.ConstraintValidatorContext
+import jakarta.validation.Payload
+import org.springframework.data.domain.Pageable
+
+/**
+ * Validates that sort properties in the annotated [Pageable] parameter match the allowed values.
+ *
+ * Apply directly on a `pageable: Pageable` parameter. The validator checks that each sort
+ * property and direction combination in the [Pageable] matches one of the strings specified
+ * in [allowedValues].
+ *
+ * Two formats are accepted in [allowedValues]:
+ * - `"property,direction"` — permits only the specific direction (e.g. `"id,asc"`, `"name,desc"`).
+ * Direction matching is case-insensitive: `"id,ASC"` and `"id,asc"` are treated identically.
+ * - `"property"` — permits any direction for that property (e.g. `"id"` matches `sort=id,asc`
+ * and `sort=id,desc`). Note: because Spring always normalises a bare `sort=id` to ascending
+ * before the validator runs, bare property names in [allowedValues] effectively allow all
+ * directions — the original omission of a direction cannot be detected.
+ *
+ * Both formats may be mixed freely. For example `["id", "name,desc"]` allows `id` in any
+ * direction but restricts `name` to descending only.
+ *
+ * @property allowedValues The allowed sort strings (e.g. `["id,asc", "id,desc"]`)
+ * @property groups Validation groups (optional)
+ * @property payload Additional payload (optional)
+ * @property message Validation error message (default: "Invalid sort column")
+ */
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+@Constraint(validatedBy = [SortValidator::class])
+@Target(AnnotationTarget.VALUE_PARAMETER)
+annotation class ValidSort(
+ val allowedValues: Array,
+ val groups: Array> = [],
+ val payload: Array> = [],
+ val message: String = "Invalid sort column"
+)
+
+class SortValidator : ConstraintValidator {
+
+ private lateinit var allowedValues: Set
+
+ override fun initialize(constraintAnnotation: ValidSort) {
+ allowedValues = constraintAnnotation.allowedValues.map { entry ->
+ DIRECTION_ASC_SUFFIX.replace(entry, ",asc")
+ .let { DIRECTION_DESC_SUFFIX.replace(it, ",desc") }
+ }.toSet()
+ }
+
+ private companion object {
+ val DIRECTION_ASC_SUFFIX = Regex(",ASC$", RegexOption.IGNORE_CASE)
+ val DIRECTION_DESC_SUFFIX = Regex(",DESC$", RegexOption.IGNORE_CASE)
+ }
+
+ override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean {
+ if (pageable == null || pageable.sort.isUnsorted) return true
+
+ val invalid = pageable.sort
+ .foldIndexed(emptyMap()) { index, acc, order ->
+ val sortValue = "${order.property},${order.direction.name.lowercase()}"
+ // Accept "property,direction" (exact match) OR "property" alone (any direction allowed)
+ if (sortValue !in allowedValues && order.property !in allowedValues) acc + (index to order.property)
+ else acc
+ }
+ .toSortedMap()
+
+ if (invalid.isNotEmpty()) {
+ context.disableDefaultConstraintViolation()
+ invalid.forEach { (index, property) ->
+ context.buildConstraintViolationWithTemplate(
+ "${context.defaultConstraintMessageTemplate} [$property]"
+ )
+ .addPropertyNode("sort")
+ .addPropertyNode("property")
+ .inIterable()
+ .atIndex(index)
+ .addConstraintViolation()
+ }
+ }
+
+ return invalid.isEmpty()
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/Pet.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/Pet.kt
new file mode 100644
index 000000000000..5e896ae0469b
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/Pet.kt
@@ -0,0 +1,34 @@
+package org.openapitools.model
+
+import java.util.Objects
+import com.fasterxml.jackson.annotation.JsonProperty
+import jakarta.validation.constraints.DecimalMax
+import jakarta.validation.constraints.DecimalMin
+import jakarta.validation.constraints.Email
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.NotNull
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.Size
+import jakarta.validation.Valid
+
+/**
+ *
+ * @param name
+ * @param id
+ * @param status pet status in the store
+ */
+data class Pet(
+
+ @get:JsonProperty("name", required = true) val name: kotlin.String,
+
+ @get:JsonProperty("id") val id: kotlin.Long? = null,
+
+ @get:JsonProperty("status") val status: kotlin.String? = null
+) : java.io.Serializable {
+
+ companion object {
+ private const val serialVersionUID: kotlin.Long = 1
+ }
+}
+
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSort.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSort.kt
new file mode 100644
index 000000000000..06ce004b5a90
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSort.kt
@@ -0,0 +1,37 @@
+package org.openapitools.model
+
+import java.util.Objects
+import com.fasterxml.jackson.annotation.JsonValue
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonProperty
+import jakarta.validation.constraints.DecimalMax
+import jakarta.validation.constraints.DecimalMin
+import jakarta.validation.constraints.Email
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.NotNull
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.Size
+import jakarta.validation.Valid
+
+/**
+*
+* Values: idCommaAsc,idCommaDesc,createdAtCommaAsc,createdAtCommaDesc
+*/
+enum class PetSort(@get:JsonValue val value: kotlin.String) : java.io.Serializable {
+
+ idCommaAsc("id,asc"),
+ idCommaDesc("id,desc"),
+ createdAtCommaAsc("createdAt,asc"),
+ createdAtCommaDesc("createdAt,desc");
+
+ companion object {
+ @JvmStatic
+ @JsonCreator
+ fun forValue(value: kotlin.String): PetSort {
+ return values().firstOrNull{it -> it.value == value}
+ ?: throw IllegalArgumentException("Unexpected value '$value' for enum 'PetSort'")
+ }
+ }
+}
+
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSortEnum.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSortEnum.kt
new file mode 100644
index 000000000000..a8bb6d27cbc9
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSortEnum.kt
@@ -0,0 +1,37 @@
+package org.openapitools.model
+
+import java.util.Objects
+import com.fasterxml.jackson.annotation.JsonValue
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonProperty
+import jakarta.validation.constraints.DecimalMax
+import jakarta.validation.constraints.DecimalMin
+import jakarta.validation.constraints.Email
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.NotNull
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.Size
+import jakarta.validation.Valid
+
+/**
+*
+* Values: nameCommaAsc,nameCommaDesc,idCommaAsc,idCommaDesc
+*/
+enum class PetSortEnum(@get:JsonValue val value: kotlin.String) : java.io.Serializable {
+
+ nameCommaAsc("name,asc"),
+ nameCommaDesc("name,desc"),
+ idCommaAsc("id,asc"),
+ idCommaDesc("id,desc");
+
+ companion object {
+ @JvmStatic
+ @JsonCreator
+ fun forValue(value: kotlin.String): PetSortEnum {
+ return values().firstOrNull{it -> it.value == value}
+ ?: throw IllegalArgumentException("Unexpected value '$value' for enum 'PetSortEnum'")
+ }
+ }
+}
+
diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/test/kotlin/org/openapitools/api/PetApiValidationTest.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/test/kotlin/org/openapitools/api/PetApiValidationTest.kt
new file mode 100644
index 000000000000..e3f24334b53c
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/test/kotlin/org/openapitools/api/PetApiValidationTest.kt
@@ -0,0 +1,177 @@
+package org.openapitools.api
+
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+
+/**
+ * Verifies the runtime behaviour of the annotations generated onto [PetApi]:
+ *
+ * - **@ValidSort** — invalid sort field/direction combinations are rejected with 400.
+ * - **@ValidPageable** — page number or size that exceeds the configured limit is rejected with 400.
+ * - **@PageableDefault** — when page/size query params are absent, the configured defaults are
+ * forwarded to the controller method (verified by assertions inside [PetApiImpl]).
+ * - **@SortDefault / @SortDefaults** — when the sort query param is absent, the configured
+ * default sort order is forwarded to the controller method (verified inside [PetApiImpl]).
+ *
+ * HTTP 200 responses confirm both that the request was accepted *and* that [PetApiImpl]'s
+ * internal assertions about the received defaults passed.
+ * HTTP 400 responses confirm that the constraint annotation rejected the invalid input.
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+class PetApiValidationTest {
+
+ @Autowired
+ lateinit var mockMvc: MockMvc
+
+ // ── @ValidSort ────────────────────────────────────────────────────────────
+ // Endpoint: GET /pet/findWithArraySortEnum allowed: id,asc | id,desc | name,asc | name,desc
+
+ @Test
+ fun `ValidSort - valid sort value returns 200`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") {
+ param("sort", "id,asc")
+ }.andExpect { status { isOk() } }
+ }
+
+ @Test
+ fun `ValidSort - multiple valid sort values return 200`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") {
+ param("sort", "id,desc")
+ param("sort", "name,asc")
+ }.andExpect { status { isOk() } }
+ }
+
+ @Test
+ fun `ValidSort - invalid sort property returns 400`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") {
+ param("sort", "unknown,asc")
+ }.andExpect { status { isBadRequest() } }
+ }
+
+ @Test
+ fun `ValidSort - invalid sort direction returns 400`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") {
+ param("sort", "id,random")
+ }.andExpect { status { isBadRequest() } }
+ }
+
+ @Test
+ fun `ValidSort - one invalid sort among multiple valid values returns 400`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") {
+ param("sort", "id,asc")
+ param("sort", "unknown,desc")
+ }.andExpect { status { isBadRequest() } }
+ }
+
+ // ── @ValidPageable — size constraint only ─────────────────────────────────
+ // Endpoint: GET /pet/findWithSizeConstraint maxSize = 100
+
+ @Test
+ fun `ValidPageable - size below maximum returns 200`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT}") {
+ param("size", "50")
+ }.andExpect { status { isOk() } }
+ }
+
+ @Test
+ fun `ValidPageable - size at maximum returns 200`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT}") {
+ param("size", "100")
+ }.andExpect { status { isOk() } }
+ }
+
+ @Test
+ fun `ValidPageable - size exceeds maximum returns 400`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT}") {
+ param("size", "101")
+ }.andExpect { status { isBadRequest() } }
+ }
+
+ @Test
+ fun `ValidPageable - unpaged Pageable is allowed (no params, no PageableDefault)`() {
+ // When no pagination parameters are supplied and no @PageableDefault is configured,
+ // Spring resolves Pageable.unpaged(). The validator must not throw and must return valid.
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT}")
+ .andExpect { status { isOk() } }
+ }
+
+ // ── @ValidPageable — size and page constraints combined ───────────────────
+ // Endpoint: GET /pet/findWithPageAndSizeConstraint maxSize = 50, maxPage = 999
+
+ @Test
+ fun `ValidPageable - size and page at their maximums return 200`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT}") {
+ param("size", "50")
+ param("page", "999")
+ }.andExpect { status { isOk() } }
+ }
+
+ @Test
+ fun `ValidPageable - size exceeds maximum for combined constraint returns 400`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT}") {
+ param("size", "51")
+ }.andExpect { status { isBadRequest() } }
+ }
+
+ @Test
+ fun `ValidPageable - page exceeds maximum returns 400`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT}") {
+ param("page", "1000")
+ }.andExpect { status { isBadRequest() } }
+ }
+
+ // ── @PageableDefault ─────────────────────────────────────────────────────
+ // Endpoint: GET /pet/findWithPageSizeDefaultsOnly @PageableDefault(page = 0, size = 25)
+ // PetApiImpl asserts page == 0 and size == 25; returns 200 on success, throws on mismatch.
+
+ @Test
+ fun `PageableDefault - absent params resolve to configured page and size defaults`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY}")
+ .andExpect { status { isOk() } }
+ }
+
+ // ── @SortDefault ─────────────────────────────────────────────────────────
+ // Endpoint: GET /pet/findWithSortDefaultOnly @SortDefault(sort = ["name"], direction = DESC)
+ // PetApiImpl asserts name DESC; returns 200 on success, throws on mismatch.
+
+ @Test
+ fun `SortDefault - absent sort param resolves to name DESC default`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY}")
+ .andExpect { status { isOk() } }
+ }
+
+ // Endpoint: GET /pet/findWithSortDefaultAsc @SortDefault(sort = ["id"], direction = ASC)
+
+ @Test
+ fun `SortDefault - absent sort param resolves to id ASC default`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC}")
+ .andExpect { status { isOk() } }
+ }
+
+ // ── @SortDefault.SortDefaults ─────────────────────────────────────────────
+ // Endpoint: GET /pet/findWithMixedSortDefaults
+ // @SortDefaults(SortDefault(["name"], DESC), SortDefault(["id"], ASC))
+ // PetApiImpl asserts both orders; returns 200 on success, throws on mismatch.
+
+ @Test
+ fun `SortDefaults - absent sort param resolves all configured sort defaults`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS}")
+ .andExpect { status { isOk() } }
+ }
+
+ // ── @PageableDefault + @SortDefault.SortDefaults combined ─────────────────
+ // Endpoint: GET /pet/findWithAllDefaults
+ // @PageableDefault(page = 0, size = 10) + @SortDefaults(name DESC, id ASC)
+ // PetApiImpl asserts page, size, and both sort orders.
+
+ @Test
+ fun `PageableDefault and SortDefaults combined - absent params resolve all defaults`() {
+ mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ALL_DEFAULTS}")
+ .andExpect { status { isOk() } }
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-source-swagger1/build.gradle.kts b/samples/server/petstore/kotlin-springboot-source-swagger1/build.gradle.kts
index cd5e76efe1bd..bbf10f687c00 100644
--- a/samples/server/petstore/kotlin-springboot-source-swagger1/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-source-swagger1/build.gradle.kts
@@ -43,6 +43,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-source-swagger1/pom.xml b/samples/server/petstore/kotlin-springboot-source-swagger1/pom.xml
index 07c34bb1288e..85b4059abd00 100644
--- a/samples/server/petstore/kotlin-springboot-source-swagger1/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-source-swagger1/pom.xml
@@ -135,6 +135,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-source-swagger2/build.gradle.kts b/samples/server/petstore/kotlin-springboot-source-swagger2/build.gradle.kts
index bab28e4573e9..fd5cbc3238f8 100644
--- a/samples/server/petstore/kotlin-springboot-source-swagger2/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-source-swagger2/build.gradle.kts
@@ -43,6 +43,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-source-swagger2/pom.xml b/samples/server/petstore/kotlin-springboot-source-swagger2/pom.xml
index b8f74802da03..121e436cec3e 100644
--- a/samples/server/petstore/kotlin-springboot-source-swagger2/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-source-swagger2/pom.xml
@@ -135,6 +135,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/build.gradle.kts b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/build.gradle.kts
index 39df365c5d0b..36af32631e9a 100644
--- a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/build.gradle.kts
@@ -45,6 +45,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/pom.xml b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/pom.xml
index abd0f31c98ca..a812b05160ad 100644
--- a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/pom.xml
+++ b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/pom.xml
@@ -125,6 +125,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/src/main/kotlin/org/openapitools/api/PetApi.kt
index 5286ee01a091..46c8f24262b2 100644
--- a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/src/main/kotlin/org/openapitools/api/PetApi.kt
+++ b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/src/main/kotlin/org/openapitools/api/PetApi.kt
@@ -7,6 +7,7 @@ package org.openapitools.api
import org.openapitools.model.ModelApiResponse
import org.springframework.data.domain.Pageable
+import org.springframework.data.web.PageableDefault
import org.openapitools.model.Pet
import io.swagger.annotations.Api
import io.swagger.annotations.ApiOperation
@@ -170,7 +171,7 @@ interface PetApi {
produces = ["application/json"]
)
fun listAllPetsPaginated(@ApiParam(hidden = true) request: javax.servlet.http.HttpServletRequest,
- @ApiParam(hidden = true) pageable: Pageable): ResponseEntity>
+ @PageableDefault(page = 0, size = 20) @ApiParam(hidden = true) pageable: Pageable): ResponseEntity>
@ApiOperation(
diff --git a/samples/server/petstore/kotlin-springboot/build.gradle.kts b/samples/server/petstore/kotlin-springboot/build.gradle.kts
index b3086b53f094..7dcfd792a49e 100644
--- a/samples/server/petstore/kotlin-springboot/build.gradle.kts
+++ b/samples/server/petstore/kotlin-springboot/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.data:spring-data-commons")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api")
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/samples/server/petstore/kotlin-springboot/pom.xml b/samples/server/petstore/kotlin-springboot/pom.xml
index 7a6726d7bbfd..37342a0b679b 100644
--- a/samples/server/petstore/kotlin-springboot/pom.xml
+++ b/samples/server/petstore/kotlin-springboot/pom.xml
@@ -119,6 +119,10 @@
javax.validation
validation-api
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
javax.annotation
javax.annotation-api
diff --git a/samples/server/petstore/springboot-sort-validation/.openapi-generator-ignore b/samples/server/petstore/springboot-sort-validation/.openapi-generator-ignore
new file mode 100644
index 000000000000..7484ee590a38
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/samples/server/petstore/springboot-sort-validation/.openapi-generator/FILES b/samples/server/petstore/springboot-sort-validation/.openapi-generator/FILES
new file mode 100644
index 000000000000..7bb3955083e0
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/.openapi-generator/FILES
@@ -0,0 +1,16 @@
+README.md
+pom.xml
+src/main/java/org/openapitools/OpenApiGeneratorApplication.java
+src/main/java/org/openapitools/RFC3339DateFormat.java
+src/main/java/org/openapitools/api/ApiUtil.java
+src/main/java/org/openapitools/api/PetApi.java
+src/main/java/org/openapitools/configuration/EnumConverterConfiguration.java
+src/main/java/org/openapitools/configuration/HomeController.java
+src/main/java/org/openapitools/configuration/ValidPageable.java
+src/main/java/org/openapitools/configuration/ValidSort.java
+src/main/java/org/openapitools/model/Pet.java
+src/main/java/org/openapitools/model/PetSort.java
+src/main/java/org/openapitools/model/PetSortEnum.java
+src/main/resources/application.properties
+src/main/resources/openapi.yaml
+src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java
diff --git a/samples/server/petstore/springboot-sort-validation/.openapi-generator/VERSION b/samples/server/petstore/springboot-sort-validation/.openapi-generator/VERSION
new file mode 100644
index 000000000000..f7962df3e243
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.22.0-SNAPSHOT
diff --git a/samples/server/petstore/springboot-sort-validation/README.md b/samples/server/petstore/springboot-sort-validation/README.md
new file mode 100644
index 000000000000..7e955e89350e
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/README.md
@@ -0,0 +1,12 @@
+# OpenAPI generated server
+
+Spring Boot Server
+
+## Overview
+This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project.
+By using the [OpenAPI-Spec](https://openapis.org), you can easily generate a server stub.
+This is an example of building a OpenAPI-enabled server in Java using the SpringBoot framework.
+
+
+Start your server as a simple java application
+Change default port value in application.properties
\ No newline at end of file
diff --git a/samples/server/petstore/springboot-sort-validation/pom.xml b/samples/server/petstore/springboot-sort-validation/pom.xml
new file mode 100644
index 000000000000..adb270620c8c
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/pom.xml
@@ -0,0 +1,70 @@
+
+ 4.0.0
+ org.openapitools
+ openapi-spring
+ jar
+ openapi-spring
+ 1.0.0
+
+ 17
+ ${java.version}
+ UTF-8
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.13
+
+
+
+
+ src/main/java
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.data
+ spring-data-commons
+
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+ org.openapitools
+ jackson-databind-nullable
+ 0.2.10
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/OpenApiGeneratorApplication.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/OpenApiGeneratorApplication.java
new file mode 100644
index 000000000000..97252a8a9402
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/OpenApiGeneratorApplication.java
@@ -0,0 +1,30 @@
+package org.openapitools;
+
+import com.fasterxml.jackson.databind.Module;
+import org.openapitools.jackson.nullable.JsonNullableModule;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator;
+
+@SpringBootApplication(
+ nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class
+)
+@ComponentScan(
+ basePackages = {"org.openapitools", "org.openapitools.api" , "org.openapitools.configuration"},
+ nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class
+)
+public class OpenApiGeneratorApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OpenApiGeneratorApplication.class, args);
+ }
+
+ @Bean(name = "org.openapitools.OpenApiGeneratorApplication.jsonNullableModule")
+ public Module jsonNullableModule() {
+ return new JsonNullableModule();
+ }
+
+}
\ No newline at end of file
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/RFC3339DateFormat.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/RFC3339DateFormat.java
new file mode 100644
index 000000000000..bcd3936d8b34
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/RFC3339DateFormat.java
@@ -0,0 +1,38 @@
+package org.openapitools;
+
+import com.fasterxml.jackson.databind.util.StdDateFormat;
+
+import java.text.DateFormat;
+import java.text.FieldPosition;
+import java.text.ParsePosition;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+public class RFC3339DateFormat extends DateFormat {
+ private static final long serialVersionUID = 1L;
+ private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC");
+
+ private final StdDateFormat fmt = new StdDateFormat()
+ .withTimeZone(TIMEZONE_Z)
+ .withColonInTimeZone(true);
+
+ public RFC3339DateFormat() {
+ this.calendar = new GregorianCalendar();
+ }
+
+ @Override
+ public Date parse(String source, ParsePosition pos) {
+ return fmt.parse(source, pos);
+ }
+
+ @Override
+ public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
+ return fmt.format(date, toAppendTo, fieldPosition);
+ }
+
+ @Override
+ public Object clone() {
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/ApiUtil.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/ApiUtil.java
new file mode 100644
index 000000000000..44bf770ccc47
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/ApiUtil.java
@@ -0,0 +1,21 @@
+package org.openapitools.api;
+
+import org.springframework.web.context.request.NativeWebRequest;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class ApiUtil {
+ public static void setExampleResponse(NativeWebRequest req, String contentType, String example) {
+ try {
+ HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class);
+ if (res != null) {
+ res.setCharacterEncoding("UTF-8");
+ res.addHeader("Content-Type", contentType);
+ res.getWriter().print(example);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApi.java
new file mode 100644
index 000000000000..6f6fa07d77c9
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApi.java
@@ -0,0 +1,300 @@
+/*
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.22.0-SNAPSHOT).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.api;
+
+import org.springframework.lang.Nullable;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.openapitools.model.Pet;
+import org.openapitools.model.PetSort;
+import org.openapitools.model.PetSortEnum;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.SortDefault;
+import org.openapitools.configuration.ValidPageable;
+import org.openapitools.configuration.ValidSort;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import jakarta.annotation.Generated;
+
+@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.22.0-SNAPSHOT")
+@Validated
+@RequestMapping("${openapi.openAPIPetstoreSortValidationTest.base-path:/v2}")
+public interface PetApi {
+
+ String PATH_FIND_PETS_AUTO_DETECTED_WITH_SORT = "/pet/findAutoDetectedWithSort";
+ /**
+ * GET /pet/findAutoDetectedWithSort : Find pets with auto-detected pagination and sort enum
+ *
+ * @param status Status filter (optional)
+ * @param page (optional, default to 0)
+ * @param size (optional, default to 20)
+ * @param sort Sort order (optional)
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_AUTO_DETECTED_WITH_SORT,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsAutoDetectedWithSort(
+ @Valid @RequestParam(value = "status", required = false) @Nullable String status,
+ @Valid @RequestParam(value = "page", required = false, defaultValue = "0") Integer page,
+ @Valid @RequestParam(value = "size", required = false, defaultValue = "20") Integer size,
+ @Valid @RequestParam(value = "sort", required = false) @Nullable String sort
+ );
+
+
+ String PATH_FIND_PETS_NON_PAGINATED_WITH_SORT_ENUM = "/pet/findNonPaginatedWithSortEnum";
+ /**
+ * GET /pet/findNonPaginatedWithSortEnum : Find pets without pagination but sort param has enum — no sort validation expected
+ *
+ * @param sort Sort order with enum but no pagination (optional)
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_NON_PAGINATED_WITH_SORT_ENUM,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsNonPaginatedWithSortEnum(
+ @Valid @RequestParam(value = "sort", required = false) @Nullable String sort
+ );
+
+
+ String PATH_FIND_PETS_WITH_ALL_DEFAULTS = "/pet/findWithAllDefaults";
+ /**
+ * GET /pet/findWithAllDefaults : Find pets — page, size, and mixed sort defaults all present
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_ALL_DEFAULTS,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithAllDefaults(
+ @PageableDefault(page = 0, size = 10) @SortDefault.SortDefaults({@SortDefault(sort = {"name"}, direction = Sort.Direction.DESC), @SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM = "/pet/findWithArraySortEnum";
+ /**
+ * GET /pet/findWithArraySortEnum : Find pets with x-spring-paginated and array sort param with inline enum on items
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithArraySortEnum(
+ @ValidSort(allowedValues = {"id,asc", "id,desc", "name,asc", "name,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_ARRAY_SORT_REF_ENUM = "/pet/findWithArraySortRefEnum";
+ /**
+ * GET /pet/findWithArraySortRefEnum : Find pets with x-spring-paginated and array sort param whose items use a $ref enum
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_REF_ENUM,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithArraySortRefEnum(
+ @ValidSort(allowedValues = {"id,asc", "id,desc", "createdAt,asc", "createdAt,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_EXTERNAL_PARAM_REF_ARRAY_SORT = "/pet/findWithExternalParamRefArraySort";
+ /**
+ * GET /pet/findWithExternalParamRefArraySort : Find pets with x-spring-paginated and sort param referenced from an external components file
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_EXTERNAL_PARAM_REF_ARRAY_SORT,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithExternalParamRefArraySort(
+ @ValidSort(allowedValues = {"name,asc", "name,desc", "id,asc", "id,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS = "/pet/findWithMixedSortDefaults";
+ /**
+ * GET /pet/findWithMixedSortDefaults : Find pets — multiple sort defaults with mixed directions (array sort param)
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithMixedSortDefaults(
+ @SortDefault.SortDefaults({@SortDefault(sort = {"name"}, direction = Sort.Direction.DESC), @SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_NON_EXPLODED_EXTERNAL_PARAM_REF_ARRAY_SORT = "/pet/findWithNonExplodedExternalParamRefArraySort";
+ /**
+ * GET /pet/findWithNonExplodedExternalParamRefArraySort : Find pets with x-spring-paginated and non-exploded sort param referenced from an external components file
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_NON_EXPLODED_EXTERNAL_PARAM_REF_ARRAY_SORT,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithNonExplodedExternalParamRefArraySort(
+ @ValidSort(allowedValues = {"name,asc", "name,desc", "id,asc", "id,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT = "/pet/findWithPageAndSizeConstraint";
+ /**
+ * GET /pet/findWithPageAndSizeConstraint : Find pets — both page and size have maximum constraints
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithPageAndSizeConstraint(
+ @ValidPageable(maxSize = 50, maxPage = 999) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY = "/pet/findWithPageSizeDefaultsOnly";
+ /**
+ * GET /pet/findWithPageSizeDefaultsOnly : Find pets — page and size defaults only, no sort default
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithPageSizeDefaultsOnly(
+ @PageableDefault(page = 0, size = 25) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_REF_SORT = "/pet/findWithRefSort";
+ /**
+ * GET /pet/findWithRefSort : Find pets with x-spring-paginated and $ref sort enum
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_REF_SORT,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithRefSort(
+ @ValidSort(allowedValues = {"id,asc", "id,desc", "createdAt,asc", "createdAt,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_SIZE_CONSTRAINT = "/pet/findWithSizeConstraint";
+ /**
+ * GET /pet/findWithSizeConstraint : Find pets — size has maximum constraint only
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithSizeConstraint(
+ @ValidPageable(maxSize = 100) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC = "/pet/findWithSortDefaultAsc";
+ /**
+ * GET /pet/findWithSortDefaultAsc : Find pets — sort default only (single field, no explicit direction defaults to ASC)
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithSortDefaultAsc(
+ @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY = "/pet/findWithSortDefaultOnly";
+ /**
+ * GET /pet/findWithSortDefaultOnly : Find pets — sort default only (single field DESC, no page/size defaults)
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithSortDefaultOnly(
+ @SortDefault.SortDefaults({@SortDefault(sort = {"name"}, direction = Sort.Direction.DESC)}) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITH_SORT_ENUM = "/pet/findByStatusWithSort";
+ /**
+ * GET /pet/findByStatusWithSort : Find pets with explicit x-spring-paginated and inline sort enum
+ *
+ * @param status Status filter (optional)
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITH_SORT_ENUM,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithSortEnum(
+ @Valid @RequestParam(value = "status", required = false) @Nullable String status,
+ @ValidSort(allowedValues = {"id,asc", "id,desc", "name,asc", "name,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable
+ );
+
+
+ String PATH_FIND_PETS_WITHOUT_SORT_ENUM = "/pet/findWithoutSortEnum";
+ /**
+ * GET /pet/findWithoutSortEnum : Find pets with pagination but sort has no enum constraint
+ *
+ * @return successful operation (status code 200)
+ */
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = PetApi.PATH_FIND_PETS_WITHOUT_SORT_ENUM,
+ produces = { "application/json" }
+ )
+ ResponseEntity> findPetsWithoutSortEnum(
+ @PageableDefault(page = 0, size = 20) final Pageable pageable
+ );
+
+}
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApiController.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApiController.java
new file mode 100644
index 000000000000..bef1f3a47ab0
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApiController.java
@@ -0,0 +1,178 @@
+package org.openapitools.api;
+
+import org.openapitools.model.Pet;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Sample implementation of {@link PetApi} demonstrating that the generated
+ * annotations ({@code @ValidSort}, {@code @ValidPageable}, {@code @PageableDefault},
+ * {@code @SortDefault}) behave correctly at runtime.
+ *
+ * Methods whose endpoint carries pagination/sort defaults assert the exact expected values
+ * inside the method body. When the Spring argument resolvers apply the annotated defaults
+ * correctly, the assertions pass and HTTP 200 is returned. If a default is missing or wrong,
+ * the assertion throws {@link IllegalStateException} and the request fails with HTTP 500,
+ * causing any calling test to fail with a clear message.
+ *
+ * Methods that only carry {@code @ValidSort} / {@code @ValidPageable} constraints need no body
+ * logic — the constraint annotations reject invalid input before this code is ever reached,
+ * and the {@code DefaultExceptionHandler} maps the resulting
+ * {@link jakarta.validation.ConstraintViolationException} to HTTP 400.
+ */
+@RestController
+public class PetApiController implements PetApi {
+
+ // ── no pageable / no special defaults ────────────────────────────────────
+
+ @Override
+ public ResponseEntity> findPetsAutoDetectedWithSort(
+ String status, Integer page, Integer size, String sort) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ @Override
+ public ResponseEntity> findPetsNonPaginatedWithSortEnum(String sort) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ // ── @ValidSort only (+ @PageableDefault) ─────────────────────────────────
+
+ @Override
+ public ResponseEntity> findPetsWithArraySortEnum(Pageable pageable) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ @Override
+ public ResponseEntity> findPetsWithArraySortRefEnum(Pageable pageable) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ @Override
+ public ResponseEntity> findPetsWithExternalParamRefArraySort(Pageable pageable) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ @Override
+ public ResponseEntity> findPetsWithNonExplodedExternalParamRefArraySort(Pageable pageable) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ @Override
+ public ResponseEntity> findPetsWithRefSort(Pageable pageable) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ @Override
+ public ResponseEntity> findPetsWithSortEnum(String status, Pageable pageable) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ @Override
+ public ResponseEntity> findPetsWithoutSortEnum(Pageable pageable) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ // ── @ValidPageable only ───────────────────────────────────────────────────
+
+ @Override
+ public ResponseEntity> findPetsWithSizeConstraint(Pageable pageable) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ @Override
+ public ResponseEntity> findPetsWithPageAndSizeConstraint(Pageable pageable) {
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ // ── @PageableDefault ─────────────────────────────────────────────────────
+ // @PageableDefault(page = 0, size = 25)
+
+ @Override
+ public ResponseEntity> findPetsWithPageSizeDefaultsOnly(Pageable pageable) {
+ if (pageable.getPageNumber() != 0) {
+ throw new IllegalStateException(
+ "@PageableDefault page: expected 0, got " + pageable.getPageNumber());
+ }
+ if (pageable.getPageSize() != 25) {
+ throw new IllegalStateException(
+ "@PageableDefault size: expected 25, got " + pageable.getPageSize());
+ }
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ // ── @SortDefault ─────────────────────────────────────────────────────────
+ // @SortDefault(sort = {"name"}, direction = DESC)
+
+ @Override
+ public ResponseEntity> findPetsWithSortDefaultOnly(Pageable pageable) {
+ Sort.Order nameOrder = pageable.getSort().getOrderFor("name");
+ if (nameOrder == null || nameOrder.getDirection() != Sort.Direction.DESC) {
+ throw new IllegalStateException(
+ "@SortDefault sort: expected name DESC, got " + pageable.getSort());
+ }
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ // @SortDefault(sort = {"id"}, direction = ASC)
+
+ @Override
+ public ResponseEntity> findPetsWithSortDefaultAsc(Pageable pageable) {
+ Sort.Order idOrder = pageable.getSort().getOrderFor("id");
+ if (idOrder == null || idOrder.getDirection() != Sort.Direction.ASC) {
+ throw new IllegalStateException(
+ "@SortDefault sort: expected id ASC, got " + pageable.getSort());
+ }
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ // ── @SortDefault.SortDefaults ─────────────────────────────────────────────
+ // @SortDefaults(SortDefault(sort = {"name"}, direction = DESC), SortDefault(sort = {"id"}, direction = ASC))
+
+ @Override
+ public ResponseEntity> findPetsWithMixedSortDefaults(Pageable pageable) {
+ Sort.Order nameOrder = pageable.getSort().getOrderFor("name");
+ if (nameOrder == null || nameOrder.getDirection() != Sort.Direction.DESC) {
+ throw new IllegalStateException(
+ "@SortDefaults sort: expected name DESC, got " + pageable.getSort());
+ }
+ Sort.Order idOrder = pageable.getSort().getOrderFor("id");
+ if (idOrder == null || idOrder.getDirection() != Sort.Direction.ASC) {
+ throw new IllegalStateException(
+ "@SortDefaults sort: expected id ASC, got " + pageable.getSort());
+ }
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+
+ // ── @PageableDefault + @SortDefault.SortDefaults combined ─────────────────
+ // @PageableDefault(page = 0, size = 10)
+ // @SortDefaults(SortDefault(sort = {"name"}, direction = DESC), SortDefault(sort = {"id"}, direction = ASC))
+
+ @Override
+ public ResponseEntity> findPetsWithAllDefaults(Pageable pageable) {
+ if (pageable.getPageNumber() != 0) {
+ throw new IllegalStateException(
+ "@PageableDefault page: expected 0, got " + pageable.getPageNumber());
+ }
+ if (pageable.getPageSize() != 10) {
+ throw new IllegalStateException(
+ "@PageableDefault size: expected 10, got " + pageable.getPageSize());
+ }
+ Sort.Order nameOrder = pageable.getSort().getOrderFor("name");
+ if (nameOrder == null || nameOrder.getDirection() != Sort.Direction.DESC) {
+ throw new IllegalStateException(
+ "@SortDefaults sort: expected name DESC, got " + pageable.getSort());
+ }
+ Sort.Order idOrder = pageable.getSort().getOrderFor("id");
+ if (idOrder == null || idOrder.getDirection() != Sort.Direction.ASC) {
+ throw new IllegalStateException(
+ "@SortDefaults sort: expected id ASC, got " + pageable.getSort());
+ }
+ return ResponseEntity.ok(Collections.emptyList());
+ }
+}
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/DefaultExceptionHandler.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/DefaultExceptionHandler.java
new file mode 100644
index 000000000000..22425d6b94b0
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/DefaultExceptionHandler.java
@@ -0,0 +1,17 @@
+package org.openapitools.configuration;
+
+import jakarta.validation.ConstraintViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class DefaultExceptionHandler {
+
+ @ExceptionHandler(ConstraintViolationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public String handleConstraintViolation(ConstraintViolationException ex) {
+ return ex.getMessage();
+ }
+}
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/EnumConverterConfiguration.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/EnumConverterConfiguration.java
new file mode 100644
index 000000000000..5abc43a397e2
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/EnumConverterConfiguration.java
@@ -0,0 +1,39 @@
+package org.openapitools.configuration;
+
+import org.openapitools.model.PetSort;
+import org.openapitools.model.PetSortEnum;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.converter.Converter;
+
+/**
+ * This class provides Spring Converter beans for the enum models in the OpenAPI specification.
+ *
+ * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent
+ * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than
+ * `original` or the specification has an integer enum.
+ */
+@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration")
+public class EnumConverterConfiguration {
+
+ @Bean(name = "org.openapitools.configuration.EnumConverterConfiguration.petSortConverter")
+ Converter petSortConverter() {
+ return new Converter() {
+ @Override
+ public PetSort convert(String source) {
+ return PetSort.fromValue(source);
+ }
+ };
+ }
+ @Bean(name = "org.openapitools.configuration.EnumConverterConfiguration.petSortEnumConverter")
+ Converter petSortEnumConverter() {
+ return new Converter() {
+ @Override
+ public PetSortEnum convert(String source) {
+ return PetSortEnum.fromValue(source);
+ }
+ };
+ }
+
+}
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/HomeController.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/HomeController.java
new file mode 100644
index 000000000000..707313504790
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/HomeController.java
@@ -0,0 +1,13 @@
+package org.openapitools.configuration;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+/**
+ * Home redirection to OpenAPI api documentation
+ */
+@Controller
+public class HomeController {
+
+}
\ No newline at end of file
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidPageable.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidPageable.java
new file mode 100644
index 000000000000..04b2ce26a5fc
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidPageable.java
@@ -0,0 +1,99 @@
+package org.openapitools.configuration;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import jakarta.validation.Payload;
+import org.springframework.data.domain.Pageable;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Validates that the page number and page size in the annotated {@link Pageable} parameter do not
+ * exceed their configured maximums.
+ *
+ * Apply directly on a {@code Pageable} parameter. Each attribute is independently optional:
+ *
+ * - {@link #maxSize()} — when set (>= 0), validates {@code pageable.getPageSize() <= maxSize}
+ *
- {@link #maxPage()} — when set (>= 0), validates {@code pageable.getPageNumber() <= maxPage}
+ *
+ *
+ * Use {@link #NO_LIMIT} (= {@code -1}, the default) to leave an attribute unconstrained.
+ *
+ *
Constraining {@link #maxPage()} is useful to prevent deep-pagination attacks, where a large
+ * page offset (e.g. {@code ?page=100000&size=20}) causes an expensive {@code OFFSET} query on the
+ * database.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = {ValidPageable.PageableConstraintValidator.class})
+@Target({ElementType.PARAMETER})
+public @interface ValidPageable {
+
+ /** Sentinel value meaning no limit is applied. */
+ int NO_LIMIT = -1;
+
+ /** Maximum allowed page size, or {@link #NO_LIMIT} if unconstrained. */
+ int maxSize() default NO_LIMIT;
+
+ /** Maximum allowed page number (0-based), or {@link #NO_LIMIT} if unconstrained. */
+ int maxPage() default NO_LIMIT;
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+ String message() default "Invalid page request";
+
+ class PageableConstraintValidator implements ConstraintValidator {
+
+ private int maxSize = NO_LIMIT;
+ private int maxPage = NO_LIMIT;
+
+ @Override
+ public void initialize(ValidPageable constraintAnnotation) {
+ maxSize = constraintAnnotation.maxSize();
+ maxPage = constraintAnnotation.maxPage();
+ }
+
+ @Override
+ public boolean isValid(Pageable pageable, ConstraintValidatorContext context) {
+ if (pageable == null) {
+ return true;
+ }
+
+ if (!pageable.isPaged()) {
+ return true;
+ }
+
+ boolean valid = true;
+ context.disableDefaultConstraintViolation();
+
+ if (maxSize >= 0 && pageable.getPageSize() > maxSize) {
+ context.buildConstraintViolationWithTemplate(
+ context.getDefaultConstraintMessageTemplate()
+ + ": page size " + pageable.getPageSize()
+ + " exceeds maximum " + maxSize)
+ .addPropertyNode("size")
+ .addConstraintViolation();
+ valid = false;
+ }
+
+ if (maxPage >= 0 && pageable.getPageNumber() > maxPage) {
+ context.buildConstraintViolationWithTemplate(
+ context.getDefaultConstraintMessageTemplate()
+ + ": page number " + pageable.getPageNumber()
+ + " exceeds maximum " + maxPage)
+ .addPropertyNode("page")
+ .addConstraintViolation();
+ valid = false;
+ }
+
+ return valid;
+ }
+ }
+}
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidSort.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidSort.java
new file mode 100644
index 000000000000..abbd2f533431
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidSort.java
@@ -0,0 +1,102 @@
+package org.openapitools.configuration;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import jakarta.validation.Payload;
+import org.springframework.data.domain.Pageable;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/**
+ * Validates that sort properties in the annotated {@link Pageable} parameter match the allowed values.
+ *
+ * Apply directly on a {@code Pageable} parameter. The validator checks that each sort
+ * property and direction combination in the {@link Pageable} matches one of the strings specified
+ * in {@link #allowedValues()}.
+ *
+ *
Two formats are accepted in {@link #allowedValues()}:
+ *
+ * - {@code "property,direction"} — permits only the specific direction (e.g. {@code "id,asc"},
+ * {@code "name,desc"}). Direction matching is case-insensitive.
+ *
- {@code "property"} — permits any direction for that property (e.g. {@code "id"} matches
+ * {@code sort=id,asc} and {@code sort=id,desc}). Note: because Spring always normalises a
+ * bare {@code sort=id} to ascending before the validator runs, bare property names in
+ * {@link #allowedValues()} effectively allow all directions.
+ *
+ *
+ * Both formats may be mixed freely. For example {@code {"id", "name,desc"}} allows {@code id}
+ * in any direction but restricts {@code name} to descending only.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = {ValidSort.SortValidator.class})
+@Target({ElementType.PARAMETER})
+public @interface ValidSort {
+
+ /** The allowed sort strings (e.g. {@code {"id,asc", "id,desc"}}). */
+ String[] allowedValues();
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+ String message() default "Invalid sort column";
+
+ class SortValidator implements ConstraintValidator {
+
+ private Set allowedValues;
+
+ @Override
+ public void initialize(ValidSort constraintAnnotation) {
+ allowedValues = Arrays.stream(constraintAnnotation.allowedValues())
+ .map(entry -> entry
+ .replaceAll("(?i),ASC$", ",asc")
+ .replaceAll("(?i),DESC$", ",desc"))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public boolean isValid(Pageable pageable, ConstraintValidatorContext context) {
+ if (pageable == null || pageable.getSort().isUnsorted()) {
+ return true;
+ }
+
+ Map invalid = new TreeMap<>();
+ int[] index = {0};
+ pageable.getSort().forEach(order -> {
+ String sortValue = order.getProperty() + "," + order.getDirection().name().toLowerCase(java.util.Locale.ROOT);
+ // Accept "property,direction" (exact match) OR "property" alone (any direction allowed)
+ if (!allowedValues.contains(sortValue) && !allowedValues.contains(order.getProperty())) {
+ invalid.put(index[0], order.getProperty());
+ }
+ index[0]++;
+ });
+
+ if (!invalid.isEmpty()) {
+ context.disableDefaultConstraintViolation();
+ invalid.forEach((i, property) ->
+ context.buildConstraintViolationWithTemplate(
+ context.getDefaultConstraintMessageTemplate() + " [" + property + "]")
+ .addPropertyNode("sort")
+ .addPropertyNode("property")
+ .inIterable()
+ .atIndex(i)
+ .addConstraintViolation());
+ }
+
+ return invalid.isEmpty();
+ }
+ }
+}
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/Pet.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/Pet.java
new file mode 100644
index 000000000000..daf897833d9b
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/Pet.java
@@ -0,0 +1,142 @@
+package org.openapitools.model;
+
+import java.net.URI;
+import java.util.Objects;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import org.springframework.lang.Nullable;
+import org.openapitools.jackson.nullable.JsonNullable;
+import java.io.Serializable;
+import java.time.OffsetDateTime;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+
+
+import java.util.*;
+import jakarta.annotation.Generated;
+
+/**
+ * Pet
+ */
+
+@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.22.0-SNAPSHOT")
+public class Pet implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private @Nullable Long id;
+
+ private String name;
+
+ private @Nullable String status;
+
+ public Pet() {
+ super();
+ }
+
+ /**
+ * Constructor with only required parameters
+ */
+ public Pet(String name) {
+ this.name = name;
+ }
+
+ public Pet id(@Nullable Long id) {
+ this.id = id;
+ return this;
+ }
+
+ /**
+ * Get id
+ * @return id
+ */
+
+ @JsonProperty("id")
+ public @Nullable Long getId() {
+ return id;
+ }
+
+ @JsonProperty("id")
+ public void setId(@Nullable Long id) {
+ this.id = id;
+ }
+
+ public Pet name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Get name
+ * @return name
+ */
+ @NotNull
+ @JsonProperty("name")
+ public String getName() {
+ return name;
+ }
+
+ @JsonProperty("name")
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Pet status(@Nullable String status) {
+ this.status = status;
+ return this;
+ }
+
+ /**
+ * pet status in the store
+ * @return status
+ */
+
+ @JsonProperty("status")
+ public @Nullable String getStatus() {
+ return status;
+ }
+
+ @JsonProperty("status")
+ public void setStatus(@Nullable String status) {
+ this.status = status;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Pet pet = (Pet) o;
+ return Objects.equals(this.id, pet.id) &&
+ Objects.equals(this.name, pet.name) &&
+ Objects.equals(this.status, pet.status);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, status);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class Pet {\n");
+ sb.append(" id: ").append(toIndentedString(id)).append("\n");
+ sb.append(" name: ").append(toIndentedString(name)).append("\n");
+ sb.append(" status: ").append(toIndentedString(status)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(@Nullable Object o) {
+ return o == null ? "null" : o.toString().replace("\n", "\n ");
+ }
+}
+
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSort.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSort.java
new file mode 100644
index 000000000000..818a4c9f1024
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSort.java
@@ -0,0 +1,60 @@
+package org.openapitools.model;
+
+import java.net.URI;
+import java.util.Objects;
+import com.fasterxml.jackson.annotation.JsonValue;
+import org.openapitools.jackson.nullable.JsonNullable;
+import java.io.Serializable;
+import java.time.OffsetDateTime;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+
+
+import java.util.*;
+import jakarta.annotation.Generated;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * Gets or Sets PetSort
+ */
+
+@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.22.0-SNAPSHOT")
+public enum PetSort implements Serializable {
+
+ ID_ASC("id,asc"),
+
+ ID_DESC("id,desc"),
+
+ CREATED_AT_ASC("createdAt,asc"),
+
+ CREATED_AT_DESC("createdAt,desc");
+
+ private final String value;
+
+ PetSort(String value) {
+ this.value = value;
+ }
+
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+
+ @JsonCreator
+ public static PetSort fromValue(String value) {
+ for (PetSort b : PetSort.values()) {
+ if (b.value.equals(value)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("Unexpected value '" + value + "'");
+ }
+}
+
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSortEnum.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSortEnum.java
new file mode 100644
index 000000000000..53a4aa2146e5
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSortEnum.java
@@ -0,0 +1,60 @@
+package org.openapitools.model;
+
+import java.net.URI;
+import java.util.Objects;
+import com.fasterxml.jackson.annotation.JsonValue;
+import org.openapitools.jackson.nullable.JsonNullable;
+import java.io.Serializable;
+import java.time.OffsetDateTime;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+
+
+import java.util.*;
+import jakarta.annotation.Generated;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * Gets or Sets PetSortEnum
+ */
+
+@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.22.0-SNAPSHOT")
+public enum PetSortEnum implements Serializable {
+
+ NAME_ASC("name,asc"),
+
+ NAME_DESC("name,desc"),
+
+ ID_ASC("id,asc"),
+
+ ID_DESC("id,desc");
+
+ private final String value;
+
+ PetSortEnum(String value) {
+ this.value = value;
+ }
+
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+
+ @JsonCreator
+ public static PetSortEnum fromValue(String value) {
+ for (PetSortEnum b : PetSortEnum.values()) {
+ if (b.value.equals(value)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("Unexpected value '" + value + "'");
+ }
+}
+
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/resources/application.properties b/samples/server/petstore/springboot-sort-validation/src/main/resources/application.properties
new file mode 100644
index 000000000000..7e90813e59b2
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/resources/application.properties
@@ -0,0 +1,3 @@
+server.port=8080
+spring.jackson.date-format=org.openapitools.RFC3339DateFormat
+spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
diff --git a/samples/server/petstore/springboot-sort-validation/src/main/resources/openapi.yaml b/samples/server/petstore/springboot-sort-validation/src/main/resources/openapi.yaml
new file mode 100644
index 000000000000..a656feda6c06
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/main/resources/openapi.yaml
@@ -0,0 +1,816 @@
+openapi: 3.0.1
+info:
+ description: Test spec for generateSortValidation feature
+ title: OpenAPI Petstore - Sort Validation Test
+ version: 1.0.0
+servers:
+- url: http://petstore.swagger.io/v2
+tags:
+- description: Everything about your Pets
+ name: pet
+paths:
+ /pet/findByStatusWithSort:
+ get:
+ operationId: findPetsWithSortEnum
+ parameters:
+ - description: Status filter
+ explode: true
+ in: query
+ name: status
+ required: false
+ schema:
+ type: string
+ style: form
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 20
+ type: integer
+ style: form
+ - description: Sort order
+ explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ enum:
+ - "id,asc"
+ - "id,desc"
+ - "name,asc"
+ - "name,desc"
+ type: string
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets with explicit x-spring-paginated and inline sort enum
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findAutoDetectedWithSort:
+ get:
+ operationId: findPetsAutoDetectedWithSort
+ parameters:
+ - description: Status filter
+ explode: true
+ in: query
+ name: status
+ required: false
+ schema:
+ type: string
+ style: form
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 20
+ type: integer
+ style: form
+ - description: Sort order
+ explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ enum:
+ - "id,asc"
+ - "id,desc"
+ type: string
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets with auto-detected pagination and sort enum
+ tags:
+ - pet
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithRefSort:
+ get:
+ operationId: findPetsWithRefSort
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 20
+ type: integer
+ style: form
+ - description: Sort order
+ explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ $ref: "#/components/schemas/PetSort"
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets with x-spring-paginated and $ref sort enum
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithoutSortEnum:
+ get:
+ operationId: findPetsWithoutSortEnum
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 20
+ type: integer
+ style: form
+ - description: Sort order (no enum constraint)
+ explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ type: string
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets with pagination but sort has no enum constraint
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findNonPaginatedWithSortEnum:
+ get:
+ operationId: findPetsNonPaginatedWithSortEnum
+ parameters:
+ - description: Sort order with enum but no pagination
+ explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ enum:
+ - "id,asc"
+ - "id,desc"
+ type: string
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets without pagination but sort param has enum — no sort validation
+ expected
+ tags:
+ - pet
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithSortDefaultOnly:
+ get:
+ operationId: findPetsWithSortDefaultOnly
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ default: "name,desc"
+ type: string
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: "Find pets — sort default only (single field DESC, no page/size defaults)"
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithSortDefaultAsc:
+ get:
+ operationId: findPetsWithSortDefaultAsc
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ default: id
+ type: string
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: "Find pets — sort default only (single field, no explicit direction\
+ \ defaults to ASC)"
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithMixedSortDefaults:
+ get:
+ operationId: findPetsWithMixedSortDefaults
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ default:
+ - "name,desc"
+ - "id,asc"
+ items:
+ type: string
+ type: array
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets — multiple sort defaults with mixed directions (array sort
+ param)
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithPageSizeDefaultsOnly:
+ get:
+ operationId: findPetsWithPageSizeDefaultsOnly
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 25
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ type: string
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: "Find pets — page and size defaults only, no sort default"
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithAllDefaults:
+ get:
+ operationId: findPetsWithAllDefaults
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 10
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ default:
+ - "name,desc"
+ - "id,asc"
+ items:
+ type: string
+ type: array
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: "Find pets — page, size, and mixed sort defaults all present"
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithSizeConstraint:
+ get:
+ operationId: findPetsWithSizeConstraint
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ maximum: 100
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ type: string
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets — size has maximum constraint only
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithPageAndSizeConstraint:
+ get:
+ operationId: findPetsWithPageAndSizeConstraint
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ maximum: 999
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ maximum: 50
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ type: string
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets — both page and size have maximum constraints
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithArraySortEnum:
+ get:
+ operationId: findPetsWithArraySortEnum
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 20
+ type: integer
+ style: form
+ - description: "Sort order (multi-column, each element must be an allowed value)"
+ explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ items:
+ enum:
+ - "id,asc"
+ - "id,desc"
+ - "name,asc"
+ - "name,desc"
+ type: string
+ type: array
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets with x-spring-paginated and array sort param with inline
+ enum on items
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithArraySortRefEnum:
+ get:
+ operationId: findPetsWithArraySortRefEnum
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 20
+ type: integer
+ style: form
+ - description: "Sort order (multi-column, items $ref to PetSort enum)"
+ explode: true
+ in: query
+ name: sort
+ required: false
+ schema:
+ items:
+ $ref: "#/components/schemas/PetSort"
+ type: array
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets with x-spring-paginated and array sort param whose items
+ use a $ref enum
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithExternalParamRefArraySort:
+ get:
+ operationId: findPetsWithExternalParamRefArraySort
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 20
+ type: integer
+ style: form
+ - description: "Sort order — multi-column, each value must be one of the allowed\
+ \ enum values"
+ in: query
+ name: sort
+ required: false
+ schema:
+ default: []
+ items:
+ $ref: "#/components/schemas/PetSortEnum"
+ type: array
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets with x-spring-paginated and sort param referenced from an
+ external components file
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+ /pet/findWithNonExplodedExternalParamRefArraySort:
+ get:
+ operationId: findPetsWithNonExplodedExternalParamRefArraySort
+ parameters:
+ - explode: true
+ in: query
+ name: page
+ required: false
+ schema:
+ default: 0
+ type: integer
+ style: form
+ - explode: true
+ in: query
+ name: size
+ required: false
+ schema:
+ default: 20
+ type: integer
+ style: form
+ - description: "Sort order — non-exploded multi-column (e.g. sort=id,asc,name,desc),\
+ \ each token pair must match the allowed enum values"
+ explode: false
+ in: query
+ name: sort
+ required: false
+ schema:
+ items:
+ $ref: "#/components/schemas/PetSortEnum"
+ type: array
+ style: form
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: "#/components/schemas/Pet"
+ type: array
+ description: successful operation
+ summary: Find pets with x-spring-paginated and non-exploded sort param referenced
+ from an external components file
+ tags:
+ - pet
+ x-spring-paginated: true
+ x-accepts:
+ - application/json
+ x-tags:
+ - tag: pet
+components:
+ parameters:
+ PetSortParam:
+ description: "Sort order — multi-column, each value must be one of the allowed\
+ \ enum values"
+ in: query
+ name: sort
+ required: false
+ schema:
+ default: []
+ items:
+ $ref: "#/components/schemas/PetSortEnum"
+ type: array
+ PetSortParamNonExploded:
+ description: "Sort order — non-exploded multi-column (e.g. sort=id,asc,name,desc),\
+ \ each token pair must match the allowed enum values"
+ explode: false
+ in: query
+ name: sort
+ required: false
+ schema:
+ items:
+ $ref: "#/components/schemas/PetSortEnum"
+ type: array
+ style: form
+ schemas:
+ PetSort:
+ enum:
+ - "id,asc"
+ - "id,desc"
+ - "createdAt,asc"
+ - "createdAt,desc"
+ type: string
+ Pet:
+ example:
+ name: name
+ id: 0
+ status: status
+ properties:
+ id:
+ format: int64
+ type: integer
+ name:
+ type: string
+ status:
+ description: pet status in the store
+ type: string
+ required:
+ - name
+ type: object
+ PetSortEnum:
+ enum:
+ - "name,asc"
+ - "name,desc"
+ - "id,asc"
+ - "id,desc"
+ type: string
diff --git a/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java b/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java
new file mode 100644
index 000000000000..3681f67e7705
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java
@@ -0,0 +1,13 @@
+package org.openapitools;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class OpenApiGeneratorApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
\ No newline at end of file
diff --git a/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/api/PetApiValidationTest.java b/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/api/PetApiValidationTest.java
new file mode 100644
index 000000000000..7e3bceea5072
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/api/PetApiValidationTest.java
@@ -0,0 +1,181 @@
+package org.openapitools.api;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Verifies the runtime behaviour of the annotations generated onto {@link PetApi}:
+ *
+ *
+ * - {@code @ValidSort} — invalid sort field/direction combinations are rejected with 400.
+ * - {@code @ValidPageable} — page number or size exceeding the configured limit is rejected with 400.
+ * - {@code @PageableDefault} — when page/size query params are absent, the configured defaults are
+ * forwarded to the controller method (verified by assertions inside {@link PetApiController}).
+ * - {@code @SortDefault} / {@code @SortDefaults} — when the sort query param is absent, the configured
+ * default sort order is forwarded to the controller method (verified inside {@link PetApiController}).
+ *
+ *
+ * HTTP 200 responses confirm both that the request was accepted and that {@link PetApiController}'s
+ * internal assertions about the received defaults passed.
+ * HTTP 400 responses confirm that the constraint annotation rejected the invalid input.
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+class PetApiValidationTest {
+
+ @Autowired
+ MockMvc mockMvc;
+
+ // ── @ValidSort ────────────────────────────────────────────────────────────
+ // Endpoint: GET /pet/findWithArraySortEnum allowed: id,asc | id,desc | name,asc | name,desc
+
+ @Test
+ void validSort_validSortValueReturns200() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM)
+ .param("sort", "id,asc"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void validSort_multipleValidSortValuesReturn200() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM)
+ .param("sort", "id,desc")
+ .param("sort", "name,asc"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void validSort_invalidSortPropertyReturns400() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM)
+ .param("sort", "unknown,asc"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void validSort_invalidSortDirectionReturns400() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM)
+ .param("sort", "id,random"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void validSort_oneInvalidSortAmongMultipleValidValuesReturns400() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM)
+ .param("sort", "id,asc")
+ .param("sort", "unknown,desc"))
+ .andExpect(status().isBadRequest());
+ }
+
+ // ── @ValidPageable — size constraint only ─────────────────────────────────
+ // Endpoint: GET /pet/findWithSizeConstraint maxSize = 100
+
+ @Test
+ void validPageable_sizeBelowMaximumReturns200() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT)
+ .param("size", "50"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void validPageable_sizeAtMaximumReturns200() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT)
+ .param("size", "100"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void validPageable_sizeExceedsMaximumReturns400() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT)
+ .param("size", "101"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void validPageable_unpagedPageableIsAllowed() throws Exception {
+ // When no pagination parameters are supplied and no @PageableDefault is configured,
+ // Spring resolves Pageable.unpaged(). The validator must not throw and must return valid.
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT))
+ .andExpect(status().isOk());
+ }
+
+ // ── @ValidPageable — size and page constraints combined ───────────────────
+ // Endpoint: GET /pet/findWithPageAndSizeConstraint maxSize = 50, maxPage = 999
+
+ @Test
+ void validPageable_sizeAndPageAtTheirMaximumsReturn200() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT)
+ .param("size", "50")
+ .param("page", "999"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void validPageable_sizeExceedsMaximumForCombinedConstraintReturns400() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT)
+ .param("size", "51"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void validPageable_pageExceedsMaximumReturns400() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT)
+ .param("page", "1000"))
+ .andExpect(status().isBadRequest());
+ }
+
+ // ── @PageableDefault ─────────────────────────────────────────────────────
+ // Endpoint: GET /pet/findWithPageSizeDefaultsOnly @PageableDefault(page = 0, size = 25)
+ // PetApiController asserts page == 0 and size == 25; returns 200 on success, throws on mismatch.
+
+ @Test
+ void pageableDefault_absentParamsResolveToConfiguredPageAndSizeDefaults() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY))
+ .andExpect(status().isOk());
+ }
+
+ // ── @SortDefault ─────────────────────────────────────────────────────────
+ // Endpoint: GET /pet/findWithSortDefaultOnly @SortDefault(sort = {"name"}, direction = DESC)
+ // PetApiController asserts name DESC; returns 200 on success, throws on mismatch.
+
+ @Test
+ void sortDefault_absentSortParamResolvesToNameDescDefault() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY))
+ .andExpect(status().isOk());
+ }
+
+ // Endpoint: GET /pet/findWithSortDefaultAsc @SortDefault(sort = {"id"}, direction = ASC)
+
+ @Test
+ void sortDefault_absentSortParamResolvesToIdAscDefault() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC))
+ .andExpect(status().isOk());
+ }
+
+ // ── @SortDefault.SortDefaults ─────────────────────────────────────────────
+ // Endpoint: GET /pet/findWithMixedSortDefaults
+ // @SortDefaults(SortDefault({"name"}, DESC), SortDefault({"id"}, ASC))
+ // PetApiController asserts both orders; returns 200 on success, throws on mismatch.
+
+ @Test
+ void sortDefaults_absentSortParamResolvesAllConfiguredSortDefaults() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS))
+ .andExpect(status().isOk());
+ }
+
+ // ── @PageableDefault + @SortDefault.SortDefaults combined ─────────────────
+ // Endpoint: GET /pet/findWithAllDefaults
+ // @PageableDefault(page = 0, size = 10) + @SortDefaults(name DESC, id ASC)
+ // PetApiController asserts page, size, and both sort orders.
+
+ @Test
+ void pageableDefaultAndSortDefaults_absentParamsResolveAllDefaults() throws Exception {
+ mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ALL_DEFAULTS))
+ .andExpect(status().isOk());
+ }
+}
diff --git a/samples/server/petstore/springboot-sort-validation/src/test/resources/application.properties b/samples/server/petstore/springboot-sort-validation/src/test/resources/application.properties
new file mode 100644
index 000000000000..32b68ede4a53
--- /dev/null
+++ b/samples/server/petstore/springboot-sort-validation/src/test/resources/application.properties
@@ -0,0 +1 @@
+openapi.openAPIPetstoreSortValidationTest.base-path=
diff --git a/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApi.java
index 52dec7c6802c..7164868c12ea 100644
--- a/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApi.java
+++ b/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApi.java
@@ -8,8 +8,11 @@
import org.openapitools.model.ModelApiResponse;
import org.springframework.lang.Nullable;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
import org.springdoc.core.annotations.ParameterObject;
import org.openapitools.model.Pet;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.SortDefault;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -182,7 +185,7 @@ default ResponseEntity> findPetsByStatus(
default ResponseEntity> findPetsByTags(
@NotNull @Parameter(name = "tags", description = "Tags to filter by", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "tags", required = true) List tags,
@Parameter(name = "size", description = "A test HeaderParam for issue #8315 - must NOT be removed when x-spring-paginated:true is used.", in = ParameterIn.HEADER) @RequestHeader(value = "size", required = false) @Nullable String size,
- @ParameterObject final Pageable pageable
+ @PageableDefault(page = 0, size = 20) @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) @ParameterObject final Pageable pageable
) {
return getDelegate().findPetsByTags(tags, size, pageable);
}
diff --git a/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApiDelegate.java b/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApiDelegate.java
index ab9374eeb0ed..fc1b51ec13fc 100644
--- a/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApiDelegate.java
+++ b/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApiDelegate.java
@@ -3,8 +3,11 @@
import org.openapitools.model.ModelApiResponse;
import org.springframework.lang.Nullable;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
import org.springdoc.core.annotations.ParameterObject;
import org.openapitools.model.Pet;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.SortDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
diff --git a/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApi.java
index 52dec7c6802c..7164868c12ea 100644
--- a/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApi.java
+++ b/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApi.java
@@ -8,8 +8,11 @@
import org.openapitools.model.ModelApiResponse;
import org.springframework.lang.Nullable;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
import org.springdoc.core.annotations.ParameterObject;
import org.openapitools.model.Pet;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.SortDefault;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -182,7 +185,7 @@ default ResponseEntity> findPetsByStatus(
default ResponseEntity> findPetsByTags(
@NotNull @Parameter(name = "tags", description = "Tags to filter by", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "tags", required = true) List tags,
@Parameter(name = "size", description = "A test HeaderParam for issue #8315 - must NOT be removed when x-spring-paginated:true is used.", in = ParameterIn.HEADER) @RequestHeader(value = "size", required = false) @Nullable String size,
- @ParameterObject final Pageable pageable
+ @PageableDefault(page = 0, size = 20) @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) @ParameterObject final Pageable pageable
) {
return getDelegate().findPetsByTags(tags, size, pageable);
}
diff --git a/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApiDelegate.java b/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApiDelegate.java
index ab9374eeb0ed..fc1b51ec13fc 100644
--- a/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApiDelegate.java
+++ b/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApiDelegate.java
@@ -3,8 +3,11 @@
import org.openapitools.model.ModelApiResponse;
import org.springframework.lang.Nullable;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
import org.springdoc.core.annotations.ParameterObject;
import org.openapitools.model.Pet;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.SortDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
diff --git a/samples/server/petstore/springboot-spring-pageable-without-j8/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-spring-pageable-without-j8/src/main/java/org/openapitools/api/PetApi.java
index 953ff2a55663..65ca60f6970b 100644
--- a/samples/server/petstore/springboot-spring-pageable-without-j8/src/main/java/org/openapitools/api/PetApi.java
+++ b/samples/server/petstore/springboot-spring-pageable-without-j8/src/main/java/org/openapitools/api/PetApi.java
@@ -8,8 +8,11 @@
import org.openapitools.model.ModelApiResponse;
import org.springframework.lang.Nullable;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
import org.springdoc.core.annotations.ParameterObject;
import org.openapitools.model.Pet;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.SortDefault;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -203,7 +206,7 @@ default ResponseEntity> findPetsByStatus(
default ResponseEntity> findPetsByTags(
@NotNull @Parameter(name = "tags", description = "Tags to filter by", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "tags", required = true) List tags,
@Parameter(name = "size", description = "A test HeaderParam for issue #8315 - must NOT be removed when x-spring-paginated:true is used.", in = ParameterIn.HEADER) @RequestHeader(value = "size", required = false) @Nullable String size,
- @ParameterObject final Pageable pageable
+ @PageableDefault(page = 0, size = 20) @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) @ParameterObject final Pageable pageable
) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
diff --git a/samples/server/petstore/springboot-spring-pageable/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-spring-pageable/src/main/java/org/openapitools/api/PetApi.java
index 953ff2a55663..65ca60f6970b 100644
--- a/samples/server/petstore/springboot-spring-pageable/src/main/java/org/openapitools/api/PetApi.java
+++ b/samples/server/petstore/springboot-spring-pageable/src/main/java/org/openapitools/api/PetApi.java
@@ -8,8 +8,11 @@
import org.openapitools.model.ModelApiResponse;
import org.springframework.lang.Nullable;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
import org.springdoc.core.annotations.ParameterObject;
import org.openapitools.model.Pet;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.SortDefault;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -203,7 +206,7 @@ default ResponseEntity> findPetsByStatus(
default ResponseEntity> findPetsByTags(
@NotNull @Parameter(name = "tags", description = "Tags to filter by", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "tags", required = true) List tags,
@Parameter(name = "size", description = "A test HeaderParam for issue #8315 - must NOT be removed when x-spring-paginated:true is used.", in = ParameterIn.HEADER) @RequestHeader(value = "size", required = false) @Nullable String size,
- @ParameterObject final Pageable pageable
+ @PageableDefault(page = 0, size = 20) @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) @ParameterObject final Pageable pageable
) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {