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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
List<String> attrs = new ArrayList<>();
if (constraints.maxSize >= 0) attrs.add("maxSize = " + constraints.maxSize);
if (constraints.maxPage >= 0) attrs.add("maxPage = " + constraints.maxPage);
if (constraints.minSize >= 0) attrs.add("minSize = " + constraints.minSize);
if (constraints.minPage >= 0) attrs.add("minPage = " + constraints.minPage);
pageableAnnotations.add("@ValidPageable(" + String.join(", ", attrs) + ")");
codegenOperation.imports.add("ValidPageable");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
List<String> attrs = new ArrayList<>();
if (constraints.maxSize >= 0) attrs.add("maxSize = " + constraints.maxSize);
if (constraints.maxPage >= 0) attrs.add("maxPage = " + constraints.maxPage);
if (constraints.minSize >= 0) attrs.add("minSize = " + constraints.minSize);
if (constraints.minPage >= 0) attrs.add("minPage = " + constraints.minPage);
pageableAnnotations.add("@ValidPageable(" + String.join(", ", attrs) + ")");
codegenOperation.imports.add("ValidPageable");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.swagger.v3.oas.models.parameters.Parameter;
import org.openapitools.codegen.utils.ModelUtils;

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -71,22 +72,28 @@ public boolean hasAny() {
}

/**
* Carries max constraints for page number and page size from a pageable operation.
* {@code -1} means no constraint specified (no {@code maximum:} in the spec).
* Carries max and min constraints for page number and page size from a pageable operation.
* {@code -1} means no constraint specified (no {@code maximum:}/{@code minimum:} in the spec).
*/
public static final class PageableConstraintsData {
/** Maximum allowed page number, or {@code -1} if unconstrained. */
public final int maxPage;
/** Maximum allowed page size, or {@code -1} if unconstrained. */
public final int maxSize;
/** Minimum allowed page number, or {@code -1} if unconstrained. */
public final int minPage;
/** Minimum allowed page size, or {@code -1} if unconstrained. */
public final int minSize;

public PageableConstraintsData(int maxPage, int maxSize) {
public PageableConstraintsData(int maxPage, int maxSize, int minPage, int minSize) {
this.maxPage = maxPage;
this.maxSize = maxSize;
this.minPage = minPage;
this.minSize = minSize;
}

public boolean hasAny() {
return maxPage >= 0 || maxSize >= 0;
return maxPage >= 0 || maxSize >= 0 || minPage >= 0 || minSize >= 0;
}
}

Expand Down Expand Up @@ -155,7 +162,7 @@ public static Map<String, List<String>> scanSortValidationEnums(
}
// If the top-level schema is an array, the enum lives on its items
Schema<?> enumSchema = schema;
if (schema.getItems() != null) {
if (ModelUtils.isArraySchema(schema)) {
enumSchema = schema.getItems();
if (enumSchema.get$ref() != null) {
enumSchema = ModelUtils.getReferencedSchema(openAPI, enumSchema);
Expand Down Expand Up @@ -205,13 +212,10 @@ public static Map<String, PageableDefaultsData> scanPageableDefaults(
if (schema == null) {
continue;
}
if (schema.get$ref() != null) {
schema = ModelUtils.getReferencedSchema(openAPI, schema);
}
if (schema == null || schema.getDefault() == null) {
Object defaultValue = resolveDefault(openAPI, schema);
if (defaultValue == null) {
continue;
}
Object defaultValue = schema.getDefault();
switch (param.getName()) {
case "page":
if (defaultValue instanceof Number) {
Expand Down Expand Up @@ -256,11 +260,12 @@ public static Map<String, PageableDefaultsData> scanPageableDefaults(
}

/**
* Scans all pageable operations for {@code maximum:} constraints on {@code page} and
* {@code size} parameters.
* Scans all pageable operations for {@code maximum:} and {@code minimum:} constraints on
* {@code page} and {@code size} parameters. Values are resolved through {@code allOf} and
* {@code $ref} schemas so that constraints defined on shared component schemas are honoured.
*
* @return map from operationId to {@link PageableConstraintsData} (only operations with
* at least one {@code maximum:} constraint are included)
* at least one constraint are included)
*/
public static Map<String, PageableConstraintsData> scanPageableConstraints(
OpenAPI openAPI, boolean autoXSpringPaginated) {
Expand All @@ -279,35 +284,113 @@ public static Map<String, PageableConstraintsData> scanPageableConstraints(
}
int maxPage = -1;
int maxSize = -1;
int minPage = -1;
int minSize = -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();
BigDecimal maximum = resolveMaximum(openAPI, schema);
BigDecimal minimum = resolveMinimum(openAPI, schema);
switch (param.getName()) {
case "page":
maxPage = maximum;
if (maximum != null) maxPage = maximum.intValue();
if (minimum != null) minPage = minimum.intValue();
break;
case "size":
maxSize = maximum;
if (maximum != null) maxSize = maximum.intValue();
if (minimum != null) minSize = minimum.intValue();
break;
default:
break;
}
}
PageableConstraintsData data = new PageableConstraintsData(maxPage, maxSize);
PageableConstraintsData data = new PageableConstraintsData(maxPage, maxSize, minPage, minSize);
if (data.hasAny()) {
result.put(operationId, data);
}
}
}
return result;
}

// -------------------------------------------------------------------------
// Private schema-resolution helpers
// -------------------------------------------------------------------------

/**
* Returns the effective {@code maximum} for the given schema, resolving through a top-level
* {@code $ref} and walking any {@code allOf} items (each resolved via their own {@code $ref}).
* Per JSON Schema / OpenAPI {@code allOf} intersection semantics the most restrictive
* (smallest) value wins.
*/
private static BigDecimal resolveMaximum(OpenAPI openAPI, Schema<?> schema) {
if (schema == null) return null;
if (schema.get$ref() != null) {
schema = ModelUtils.getReferencedSchema(openAPI, schema);
if (schema == null) return null;
}
BigDecimal result = schema.getMaximum();
if (schema.getAllOf() != null) {
for (Schema<?> allOfItem : schema.getAllOf()) {
Schema<?> resolved = ModelUtils.getReferencedSchema(openAPI, allOfItem);
if (resolved != null && resolved.getMaximum() != null) {
if (result == null || resolved.getMaximum().compareTo(result) < 0) {
result = resolved.getMaximum();
}
}
}
}
return result;
}

/**
* Returns the effective {@code minimum} for the given schema, resolving through a top-level
* {@code $ref} and walking any {@code allOf} items (each resolved via their own {@code $ref}).
* Per JSON Schema / OpenAPI {@code allOf} intersection semantics the most restrictive
* (largest) value wins.
*/
private static BigDecimal resolveMinimum(OpenAPI openAPI, Schema<?> schema) {
if (schema == null) return null;
if (schema.get$ref() != null) {
schema = ModelUtils.getReferencedSchema(openAPI, schema);
if (schema == null) return null;
}
BigDecimal result = schema.getMinimum();
if (schema.getAllOf() != null) {
for (Schema<?> allOfItem : schema.getAllOf()) {
Schema<?> resolved = ModelUtils.getReferencedSchema(openAPI, allOfItem);
if (resolved != null && resolved.getMinimum() != null) {
if (result == null || resolved.getMinimum().compareTo(result) > 0) {
result = resolved.getMinimum();
}
}
}
}
return result;
}

/**
* Returns the effective {@code default} for the given schema. Unlike constraints, the inline
* schema's default takes precedence (explicit per-endpoint override); falls back to the first
* non-null default found in {@code allOf} items.
*/
private static Object resolveDefault(OpenAPI openAPI, Schema<?> schema) {
if (schema == null) return null;
if (schema.get$ref() != null) {
schema = ModelUtils.getReferencedSchema(openAPI, schema);
if (schema == null) return null;
}
if (schema.getDefault() != null) return schema.getDefault();
if (schema.getAllOf() != null) {
for (Schema<?> allOfItem : schema.getAllOf()) {
Schema<?> resolved = ModelUtils.getReferencedSchema(openAPI, allOfItem);
if (resolved != null && resolved.getDefault() != null) {
return resolved.getDefault();
}
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ 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.
* Validates that the page number and page size in the annotated {@link Pageable} parameter are
* within their configured bounds.
*
* <p>Apply directly on a {@code Pageable} parameter. Each attribute is independently optional:
* <ul>
* <li>{@link #maxSize()} — when set (&gt;= 0), validates {@code pageable.getPageSize() <= maxSize}
* <li>{@link #maxPage()} — when set (&gt;= 0), validates {@code pageable.getPageNumber() <= maxPage}
* <li>{@link #minSize()} — when set (&gt;= 0), validates {@code pageable.getPageSize() >= minSize}
* <li>{@link #minPage()} — when set (&gt;= 0), validates {@code pageable.getPageNumber() >= minPage}
* </ul>
*
* <p>Use {@link #NO_LIMIT} (= {@code -1}, the default) to leave an attribute unconstrained.
Expand All @@ -43,6 +45,12 @@ public @interface ValidPageable {
/** Maximum allowed page number (0-based), or {@link #NO_LIMIT} if unconstrained. */
int maxPage() default NO_LIMIT;

/** Minimum allowed page size, or {@link #NO_LIMIT} if unconstrained. */
int minSize() default NO_LIMIT;

/** Minimum allowed page number (0-based), or {@link #NO_LIMIT} if unconstrained. */
int minPage() default NO_LIMIT;

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
Expand All @@ -53,11 +61,15 @@ public @interface ValidPageable {

private int maxSize = NO_LIMIT;
private int maxPage = NO_LIMIT;
private int minSize = NO_LIMIT;
private int minPage = NO_LIMIT;

@Override
public void initialize(ValidPageable constraintAnnotation) {
maxSize = constraintAnnotation.maxSize();
maxPage = constraintAnnotation.maxPage();
minSize = constraintAnnotation.minSize();
minPage = constraintAnnotation.minPage();
}

@Override
Expand Down Expand Up @@ -93,6 +105,26 @@ public @interface ValidPageable {
valid = false;
}

if (minSize >= 0 && pageable.getPageSize() < minSize) {
context.buildConstraintViolationWithTemplate(
context.getDefaultConstraintMessageTemplate()
+ ": page size " + pageable.getPageSize()
+ " is below minimum " + minSize)
.addPropertyNode("size")
.addConstraintViolation();
valid = false;
}

if (minPage >= 0 && pageable.getPageNumber() < minPage) {
context.buildConstraintViolationWithTemplate(
context.getDefaultConstraintMessageTemplate()
+ ": page number " + pageable.getPageNumber()
+ " is below minimum " + minPage)
.addPropertyNode("page")
.addConstraintViolation();
valid = false;
}

return valid;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ 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.
* Validates that the page number and page size in the annotated [Pageable] parameter are within
* their configured bounds.
*
* 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`
* - [minSize] — when set (>= 0), validates `pageable.pageSize >= minSize`
* - [minPage] — when set (>= 0), validates `pageable.pageNumber >= minPage`
*
* Use [NO_LIMIT] (= -1, the default) to leave an attribute unconstrained.
*
Expand All @@ -21,6 +23,8 @@ import org.springframework.data.domain.Pageable
*
* @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 minSize Minimum allowed page size, or [NO_LIMIT] if unconstrained
* @property minPage Minimum 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")
Expand All @@ -32,6 +36,8 @@ import org.springframework.data.domain.Pageable
annotation class ValidPageable(
val maxSize: Int = ValidPageable.NO_LIMIT,
val maxPage: Int = ValidPageable.NO_LIMIT,
val minSize: Int = ValidPageable.NO_LIMIT,
val minPage: Int = ValidPageable.NO_LIMIT,
val groups: Array<kotlin.reflect.KClass<*>> = [],
val payload: Array<kotlin.reflect.KClass<out Payload>> = [],
val message: String = "Invalid page request"
Expand All @@ -45,10 +51,14 @@ class PageableConstraintValidator : ConstraintValidator<ValidPageable, Pageable>

private var maxSize = ValidPageable.NO_LIMIT
private var maxPage = ValidPageable.NO_LIMIT
private var minSize = ValidPageable.NO_LIMIT
private var minPage = ValidPageable.NO_LIMIT

override fun initialize(constraintAnnotation: ValidPageable) {
maxSize = constraintAnnotation.maxSize
maxPage = constraintAnnotation.maxPage
minSize = constraintAnnotation.minSize
minPage = constraintAnnotation.minPage
}

override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean {
Expand Down Expand Up @@ -76,6 +86,24 @@ class PageableConstraintValidator : ConstraintValidator<ValidPageable, Pageable>
valid = false
}

if (minSize >= 0 && pageable.pageSize < minSize) {
context.buildConstraintViolationWithTemplate(
"${context.defaultConstraintMessageTemplate}: page size ${pageable.pageSize} is below minimum $minSize"
)
.addPropertyNode("size")
.addConstraintViolation()
valid = false
}

if (minPage >= 0 && pageable.pageNumber < minPage) {
context.buildConstraintViolationWithTemplate(
"${context.defaultConstraintMessageTemplate}: page number ${pageable.pageNumber} is below minimum $minPage"
)
.addPropertyNode("page")
.addConstraintViolation()
valid = false
}

return valid
}
}
Loading
Loading