Filtering and Sorting
JsonApi4j supports JSON:API filtering and sorting for multi-resource operations (GET /users).
The framework handles parsing query parameters from the client and making them available to your operation code via JsonApiRequest. Your code implements the actual data filtering and sorting logic — the framework never touches your data source.
Filtering
JSON:API defines the filter[...] query parameter convention for filtering. For example:
GET /users?filter[region]=Europe
GET /users?filter[region]=Europe,Americas&filter[status]=active
GET /countries?filter[id]=US,NO,FI
Accessing Filters
In your operation, call request.getFilters() to get all filter parameters as a Map<String, List<String>>:
@Override
public PaginationAwareResponse<UserDbEntity> readPage(JsonApiRequest request) {
Map<String, List<String>> filters = request.getFilters();
// filters = {"region" -> ["Europe", "Americas"], "status" -> ["active"]}
return PaginationAwareResponse.cursorAware(
userDb.readAll(filters, request.getCursor()),
nextCursor
);
}
Each filter key is the name inside filter[...] (e.g., "region" from filter[region]). The value is a list of strings — comma-separated values in the query parameter are split automatically.
The filter[id] Convention
The filter[id] parameter has special significance in JsonApi4j. The Compound Documents resolver uses it to batch-fetch included resources:
GET /countries?filter[id]=US,NO,FI
If your resource supports compound documents (include query parameter), it’s strongly recommended to implement filter[id] support in your readPage() method:
@Override
public PaginationAwareResponse<CountryDto> readPage(JsonApiRequest request) {
List<String> filterIds = request.getFilters().get("id");
if (filterIds != null && !filterIds.isEmpty()) {
return PaginationAwareResponse.fromItemsNotPageable(
countryService.findByIds(filterIds)
);
}
return PaginationAwareResponse.cursorAware(
countryService.findAll(request.getCursor()),
nextCursor
);
}
Without this, the compound docs resolver falls back to sequential read-by-id calls — one per included resource. See Performance Tuning for details.
Validation
The framework does not validate filter names or values — any filter[...] parameter is parsed and passed through. If you need to reject unknown filters or validate values, do so in your operation’s validateReadMultiple() method:
@Override
public void validateReadMultiple(JsonApiRequest request) {
ReadMultipleResourcesOperation.DEFAULT_VALIDATOR.accept(request);
Set<String> allowedFilters = Set.of("id", "region", "status");
for (String filterName : request.getFilters().keySet()) {
if (!allowedFilters.contains(filterName)) {
throw new ConstraintViolationException(
DefaultErrorCodes.MISSING_REQUIRED_PARAMETER,
"Unknown filter: " + filterName,
"filter[" + filterName + "]"
);
}
}
}
Sorting
JSON:API defines the sort query parameter for ordering results. Fields are comma-separated, with an optional - prefix for descending order:
GET /users?sort=lastName → ascending by lastName
GET /users?sort=-createdAt → descending by createdAt
GET /users?sort=region,-lastName → ascending by region, then descending by lastName
Accessing Sort Parameters
Call request.getSortBy() to get an ordered Map<String, SortOrder>:
@Override
public PaginationAwareResponse<UserDbEntity> readPage(JsonApiRequest request) {
Map<String, SortOrder> sortBy = request.getSortBy();
// sortBy = {"region" -> ASC, "lastName" -> DESC}
return PaginationAwareResponse.cursorAware(
userDb.readAll(request.getFilters(), sortBy, request.getCursor()),
nextCursor
);
}
SortOrder is an enum with two values: ASC and DESC.
Sort Limit
The framework enforces a global cap of 5 sort fields per request (SortAwareRequest.NUMBER_OF_SORT_BY_GLOBAL_CAP). Requests with more than 5 sort fields are rejected.
Utility Methods
SortAwareRequest provides static helpers for working with sort parameters:
| Method | Description |
|---|---|
extractSortBy(sortByParam) |
Strips the - or + prefix, returning the field name |
extractSortOrder(sortByParam) |
Returns DESC for - prefix, ASC otherwise |
wrapWithSortOrder(sortBy, sortOrder) |
Wraps a field with - for DESC, plain for ASC |
These are useful when building downstream queries or forwarding sort parameters to other services.
Combining Filters, Sorting, and Pagination
All three features work together naturally. The framework preserves filter and sort parameters in pagination links:
GET /users?filter[region]=Europe&sort=-createdAt&page[cursor]=DoJu
Response links:
"links": {
"self": "/users?filter%5Bregion%5D=Europe&sort=-createdAt&page%5Bcursor%5D=DoJu",
"next": "/users?filter%5Bregion%5D=Europe&sort=-createdAt&page%5Bcursor%5D=DoJw"
}
A typical readPage() implementation that supports all three:
@Override
public PaginationAwareResponse<UserDbEntity> readPage(JsonApiRequest request) {
Map<String, List<String>> filters = request.getFilters();
Map<String, SortOrder> sortBy = request.getSortBy();
DbPage<UserDbEntity> page = userDb.query(filters, sortBy, request.getCursor());
return PaginationAwareResponse.cursorAware(page.getItems(), page.getNextCursor());
}