Spring Boot
Sorting
Spring Data resolves Sort from request parameters automatically when Pageable is used, or as a standalone method parameter. This entry covers Sort binding, multi-field sorting, whitelisting sortable fields to prevent injection, default sort configuration, and dynamic sort construction in Specifications.
Sort via Pageable
When Pageable is bound from the request, the sort parameter is resolved alongside page and size. Multiple sort parameters can be supplied to sort by several fields. Each value takes the form field,direction where direction is asc or desc.
Java
// ── Request examples ──────────────────────────────────────────────────
// GET /users?sort=name,asc
// GET /users?sort=createdAt,desc
// GET /users?sort=lastName,asc&sort=firstName,asc (multi-field)
// GET /users?page=0&size=20&sort=createdAt,desc
@GetMapping
public ResponseEntity<Page<UserResponse>> findAll(
@PageableDefault(
size = 20,
sort = {"createdAt", "id"}, // default: createdAt DESC, id DESC
direction = Sort.Direction.DESC
) Pageable pageable) {
return ResponseEntity.ok(
PageResponse.from(userService.findAll(pageable)));
}
// ── What Spring passes to the repository ─────────────────────────────
// Pageable internally carries a Sort object:
// Sort.by(Sort.Order.desc("createdAt"), Sort.Order.asc("name"))
// JPA translates this to: ORDER BY created_at DESC, name ASCStandalone Sort Parameter
For endpoints that stream or return a plain List rather than a Page, bind Sort directly as a method parameter. Spring resolves it from the same sort query parameters using SortHandlerMethodArgumentResolver.
Java
// ── Controller ────────────────────────────────────────────────────────
@GetMapping("/all")
public ResponseEntity<List<UserResponse>> findAll(
@SortDefault(sort = "name", direction = Sort.Direction.ASC) Sort sort) {
return ResponseEntity.ok(userService.findAll(sort));
}
// ── Repository ────────────────────────────────────────────────────────
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findAll(Sort sort);
List<User> findByStatus(UserStatus status, Sort sort);
}
// ── Service ───────────────────────────────────────────────────────────
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepo;
public List<UserResponse> findAll(Sort sort) {
return userRepo.findAll(sort).stream()
.map(UserResponse::from)
.toList();
}
}
// ── Programmatic Sort construction ────────────────────────────────────
Sort byName = Sort.by("name"); // ASC by default
Sort byNameDesc = Sort.by(Sort.Direction.DESC, "name");
Sort multi = Sort.by(
Sort.Order.desc("createdAt"),
Sort.Order.asc("name").ignoreCase()
);
Sort unsorted = Sort.unsorted();Whitelisting Sortable Fields
Never pass client-supplied sort fields directly to JPA — a malicious client could sort by a sensitive column, trigger a costly expression, or probe the schema. Validate the field name against an explicit allowlist before building the Sort. Reject or silently replace disallowed fields.
Java
// ── Allowlist validator component ────────────────────────────────────
@Component
public class SortValidator {
public Sort validate(Pageable pageable, Set<String> allowed) {
List<Sort.Order> safe = pageable.getSort().stream()
.filter(order -> allowed.contains(order.getProperty()))
.toList();
if (safe.isEmpty()) {
// Fall back to a safe default when nothing passes
return Sort.by(Sort.Direction.DESC, "createdAt");
}
return Sort.by(safe);
}
}
// ── Controller using the validator ────────────────────────────────────
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final SortValidator sortValidator;
private static final Set<String> SORTABLE_FIELDS = Set.of(
"id", "name", "email", "createdAt", "status"
);
@GetMapping
public ResponseEntity<Page<UserResponse>> findAll(
@PageableDefault(size = 20) Pageable pageable) {
Sort safeSort = sortValidator.validate(pageable, SORTABLE_FIELDS);
Pageable safePaged = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
safeSort);
return ResponseEntity.ok(
PageResponse.from(userService.findAll(safePaged)));
}
}Sorting with Specifications
When using JPA Specifications for dynamic queries, pass the Sort inside a Pageable or construct it programmatically. The Specification and the Sort are independent — the Specification defines the WHERE clause; the Sort defines the ORDER BY clause.
Java
// ── Specification factory ─────────────────────────────────────────────
public class UserSpecifications {
public static Specification<User> hasStatus(UserStatus status) {
return (root, query, cb) ->
status == null ? null : cb.equal(root.get("status"), status);
}
public static Specification<User> nameLike(String name) {
return (root, query, cb) ->
name == null ? null
: cb.like(cb.lower(root.get("name")),
"%" + name.toLowerCase() + "%");
}
}
// ── Repository ────────────────────────────────────────────────────────
public interface UserRepository
extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {}
// ── Service — combining Specification with Sort ───────────────────────
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepo;
public Page<UserResponse> search(UserStatus status,
String name,
Pageable pageable) {
Specification<User> spec = Specification
.where(UserSpecifications.hasStatus(status))
.and(UserSpecifications.nameLike(name));
return userRepo.findAll(spec, pageable) // pageable carries sort
.map(UserResponse::from);
}
}
// ── Controller ────────────────────────────────────────────────────────
@GetMapping
public ResponseEntity<Page<UserResponse>> search(
@RequestParam(required = false) UserStatus status,
@RequestParam(required = false) String name,
@PageableDefault(size = 20, sort = "name",
direction = Sort.Direction.ASC) Pageable pageable) {
return ResponseEntity.ok(
PageResponse.from(userService.search(status, name, pageable)));
}Sorting on DTOs with Column Mapping
Client-facing field names in a DTO often differ from the JPA entity property names. Map DTO field names to entity property names before passing the Sort to the repository, so ORDER BY references valid entity columns rather than unknown JSON keys.
Java
// ── DTO field → entity property mapping ──────────────────────────────
@Component
public class UserSortMapper {
private static final Map<String, String> FIELD_MAP = Map.of(
"fullName", "name", // DTO "fullName" → entity "name"
"joinDate", "createdAt", // DTO "joinDate" → entity "createdAt"
"dept", "department.name"// nested property
);
public Sort toEntitySort(Sort dtoSort) {
List<Sort.Order> mapped = dtoSort.stream()
.map(order -> {
String entityField = FIELD_MAP.getOrDefault(
order.getProperty(), order.getProperty());
return order.isAscending()
? Sort.Order.asc(entityField)
: Sort.Order.desc(entityField);
})
.toList();
return Sort.by(mapped);
}
}
// ── Controller ────────────────────────────────────────────────────────
@GetMapping
public ResponseEntity<Page<UserResponse>> findAll(
@PageableDefault(size = 20) Pageable pageable) {
Sort entitySort = sortMapper.toEntitySort(pageable.getSort());
Pageable mapped = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
entitySort);
return ResponseEntity.ok(
PageResponse.from(userService.findAll(mapped)));
}
// ── Example: GET /users?sort=fullName,asc
// Client sends "fullName", mapper translates to entity "name"
// JPA generates: ORDER BY name ASC