Pageable Input Validation: Designing a PageRequestDto
- Published on
Junyoung Yang
While implementing pagination for a gift API in Kakao Tech Campus, I had to think about how much of the request parameters should be accepted as-is.
At first, receiving Spring's Pageable directly in the controller felt convenient. page, size, and sort are automatically bound, so the code becomes simple. Page parameters look like simple numbers at first, so I did not think much about them. But if they are left open as-is, the API's rules for allowed sort fields and page size can become unclear more easily than expected.
From the API user's point of view, exposing Pageable directly was not always the best choice. If a user sends a sort field that should not be allowed, or requests an extremely large page size, the API needs a clear rule for how to handle it.
Problem
For example, assume there is an API like this.
GET /api/wishlist?page=0&size=3&sort=product.name,desc
If a user intentionally sends strange values, several problems can happen.
- They can put an unsupported field into
sort. - They can send too many sort conditions.
- They can request an excessively large value like
size=100000.
Spring's default Pageable is convenient, but request parameters are bound almost directly. If the API does not define allowed sort fields and page size rules separately, the allowed range can become vague.
Approach
The direction was to create a separate request DTO and limit the sortable fields and page size there.
First, I defined a common interface so that sortable fields could be managed with enums.
public interface SortField {
String getFieldName();
}
Then I defined the allowed sort fields for each domain as an enum.
public enum WishlistSortField implements SortField {
CREATED_AT("createdAt"),
PRODUCT_NAME("product.name"),
PRODUCT_PRICE("product.price");
private final String fieldName;
WishlistSortField(String fieldName) {
this.fieldName = fieldName;
}
@Override
public String getFieldName() {
return fieldName;
}
}
After that, the request DTO received page, size, sortBy, and ascending, and converted them into a usable PageRequest.
public record PageRequestDto(
@Min(value = 1, message = "페이지 번호는 1 이상이어야 한다.")
Integer page,
@Range(min = 1, max = 100, message = "페이지 크기는 1 이상 100 이하여야 한다.")
Integer size,
String sortBy,
Boolean ascending
) {
public <T extends Enum<T> & SortField> PageRequest toSafePageable(
Class<T> enumClass, T defaultSortField) {
int page = this.page != null ? this.page : 1;
int size = this.size != null ? this.size : 10;
T sortField = defaultSortField;
boolean ascending = this.ascending != null ? this.ascending : true;
if (sortBy != null) {
for (T enumValue : enumClass.getEnumConstants()) {
if (enumValue.getFieldName().equals(sortBy)) {
sortField = enumValue;
}
}
}
return PageRequest.of(page - 1, size,
Sort.by(
ascending ? Sort.Direction.ASC : Sort.Direction.DESC,
sortField.getFieldName()
)
);
}
}
After Applying
With this structure, the page request rules can be cleaned up once when the request enters the controller.
@GetMapping
public Page<WishlistItemDto> getWishlistItems(
@Valid PageRequestDto pageRequest) {
Pageable pageable = pageRequest.toSafePageable(
WishlistSortField.class,
WishlistSortField.CREATED_AT
);
return wishlistService.getItems(pageable);
}
The inputs I wanted to block also became clearer.
| Input | Handling |
|---|---|
size=100000 | Limit to the maximum allowed size |
sort=malicious_field | Use the default value if it is not in the enum |
Multiple sort parameters | Handle only a single sortBy value |
page=-1 | Limit with a minimum value |
In the service layer, I could assume that the pageable value had already been cleaned up once, which helped keep the service logic simpler.
Takeaway
This work was not about building a huge security feature. It was closer to defining what range of input the API should accept.
Spring's Pageable is useful, but if external input is left open as-is, the API's allowed range can become unclear. By defining the page size, allowed sort fields, and default sort field myself, I could treat input validation as part of API design.