JUN0.DEV
JUN0.DEV

Pageable Input Validation: Designing a PageRequestDto

Published on
  • avatarJunyoung 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.

InputHandling
size=100000Limit to the maximum allowed size
sort=malicious_fieldUse the default value if it is not in the enum
Multiple sort parametersHandle only a single sortBy value
page=-1Limit 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.