Annotation-driven filtering, sorting, and pagination for Spring WebFlux + Reactive MongoDB.
No existing library covers WebFlux + Reactive MongoDB + REST query parameter filtering together. This library fills that gap with a clean, annotation-driven API.
- Features
- Compatibility
- Getting Started
- Usage
- Query Parameters
- API Reference
- Advanced Configuration
- Error Handling
- Security
- Building from Source
- Contributing
- License
- Filtering via query parameters:
filter[field][operator]=value - Sorting with multi-field support:
sort=field,asc|desc - Pagination with configurable max page size
- Field whitelisting per entity via
EntityFilterSpec @Filteredannotation for zero-boilerplate controller integration- Spring Boot auto-configuration -- no
@ComponentScanor@Importrequired - Injection-safe -- field whitelist, operator whitelist, type conversion, regex escaping
| Library Version | Java | Spring Boot | Spring Data MongoDB Reactive |
|---|---|---|---|
| 1.0.x | 21+ | 4.x | 5.x |
<dependency>
<groupId>de.magicthings</groupId>
<artifactId>spring-reactive-mongo-filter</artifactId>
<version>1.0.0</version>
</dependency>implementation("de.magicthings:spring-reactive-mongo-filter:1.0.0")implementation 'de.magicthings:spring-reactive-mongo-filter:1.0.0'All required beans are auto-configured when ReactiveMongoTemplate is on the classpath. No additional setup needed.
Create a @Component that implements EntityFilterSpec<T> to declare which fields are filterable and sortable:
@Component
public class ProductFilterSpec implements EntityFilterSpec<Product> {
private static final Map<String, FilterableField> FILTERABLE_FIELDS = Map.of(
"name", FilterableField.string("name"),
"category", FilterableField.string("category"),
"price", FilterableField.integer("price"),
"active", FilterableField.bool("active"),
"created", FilterableField.dateTime("created")
);
private static final Map<String, String> SORTABLE_FIELDS = Map.of(
"name", "name",
"price", "price",
"created", "created"
);
@Override
public Map<String, FilterableField> getFilterableFields() {
return FILTERABLE_FIELDS;
}
@Override
public Map<String, String> getSortableFields() {
return SORTABLE_FIELDS;
}
}Use @Filtered to resolve a FilterRequest from query parameters automatically:
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping
public Mono<FilteredPage<Product>> getAll(
@Filtered(ProductFilterSpec.class) FilterRequest filterRequest) {
return productService.findAll(filterRequest);
}
}@Service
@RequiredArgsConstructor
public class ProductService {
private final ReactiveFilterRepository filterRepository;
private final ProductFilterSpec filterSpec;
public Mono<FilteredPage<Product>> findAll(FilterRequest filterRequest) {
return filterRepository.findFiltered(filterRequest, filterSpec, Product.class);
}
}That's it -- no parser setup, no criteria building, no pagination math.
Example request:
GET /api/products?filter[active][eq]=true&filter[price][lte]=50&sort=created,desc&page=0&size=10
GET /api/products?filter[name][like]=phone
GET /api/products?filter[category][eq]=electronics
GET /api/products?filter[active][eq]=true
GET /api/products?filter[created][gte]=2025-01-01T00:00:00
GET /api/products?filter[category][in]=electronics,books,toys
Multiple filters are combined with AND logic:
GET /api/products?filter[active][eq]=true&filter[price][lte]=100
| Operator | Description | Supported Types |
|---|---|---|
eq |
Equals | String, Boolean, DateTime, Integer, Long |
neq |
Not equals | String, Integer, Long |
like |
Contains (case-insensitive) | String |
gt |
Greater than | DateTime, Integer, Long |
lt |
Less than | DateTime, Integer, Long |
gte |
Greater than or equal | DateTime, Integer, Long |
lte |
Less than or equal | DateTime, Integer, Long |
in |
In list (comma-separated) | String, Integer, Long |
GET /api/products?sort=price,asc
GET /api/products?sort=price,asc&sort=created,desc
Default direction is asc. Multi-sort is supported by repeating the sort parameter. When no sort parameter is provided, the default sort from EntityFilterSpec.getDefaultSort() is applied (created DESC).
| Parameter | Default | Max | Description |
|---|---|---|---|
page |
0 |
-- | Page number (0-indexed) |
size |
20 |
100 |
Items per page |
Full Javadoc is available at javadoc.io.
Interface to implement per entity. Defines the whitelist of filterable and sortable fields.
| Method | Description | Default |
|---|---|---|
getFilterableFields() |
Returns allowed filter fields and their types | (required) |
getSortableFields() |
Returns allowed sort fields (param -> mongoField) |
(required) |
getDefaultSort() |
Sort applied when no sort param is given |
created DESC |
getBaseCriteria() |
Criteria added to every query (e.g. soft-delete) | deleted: false |
Defines a filterable field with its type and allowed operators. Factory methods accept either a single name (used as both query parameter and MongoDB field) or a pair (paramName, documentField) when they differ.
| Factory Method | Operators |
|---|---|
FilterableField.string(name) |
eq, neq, like, in |
FilterableField.string(param, field) |
eq, neq, like, in |
FilterableField.bool(name) |
eq |
FilterableField.bool(param, field) |
eq |
FilterableField.dateTime(name) |
eq, gt, lt, gte, lte |
FilterableField.dateTime(param, field) |
eq, gt, lt, gte, lte |
FilterableField.integer(name) |
eq, neq, gt, lt, gte, lte, in |
FilterableField.longType(name) |
eq, neq, gt, lt, gte, lte, in |
Note:
integerandlongTypeonly support the single-name variant. Use theFilterableFieldconstructor directly if you need a different parameter-to-field mapping for these types.
Record returned by ReactiveFilterRepository.findFiltered():
public record FilteredPage<T>(
List<T> content,
int pageNumber,
int pageSize,
long totalElements,
int totalPages
)| Method | Description |
|---|---|
isFirst() |
true if this is the first page |
isLast() |
true if this is the last page |
hasNext() |
true if a next page exists |
hasPrevious() |
true if a previous page exists |
Main query execution component. Auto-configured as a Spring bean.
public <T> Mono<FilteredPage<T>> findFiltered(
FilterRequest filterRequest,
EntityFilterSpec<T> spec,
Class<T> entityClass,
Criteria... additional
)The additional varargs allows adding programmatic constraints on top of the user's filters (e.g. for multi-tenancy):
filterRepository.findFiltered(
filterRequest, productFilterSpec, Product.class,
Criteria.where("tenantId").is(currentTenantId)
);Parameter annotation for WebFlux controller methods. Automatically resolves a FilterRequest from query parameters using the specified EntityFilterSpec. The referenced spec class must be a Spring-managed bean (@Component).
@GetMapping
public Mono<FilteredPage<Product>> list(
@Filtered(ProductFilterSpec.class) FilterRequest filterRequest) {
// filterRequest is fully parsed and validated
}When the query parameter name differs from the MongoDB document field:
FilterableField.string("name", "displayName") // ?filter[name][eq]=... queries "displayName"
FilterableField.dateTime("modified", "lastModified")By default, deleted: false is added to every query. Override getBaseCriteria() to change or disable this:
@Override
public List<Criteria> getBaseCriteria() {
return List.of(); // no base criteria
}@Override
public List<SortCriteria> getDefaultSort() {
return List.of(new SortCriteria("name", Sort.Direction.ASC));
}All beans are registered with @ConditionalOnMissingBean. Define your own to customize behavior:
@Bean
public FilterRequestParser filterRequestParser() {
return new CustomFilterRequestParser();
}Invalid filter requests throw IllegalArgumentException with descriptive messages:
| Error | Example Message |
|---|---|
| Unknown filter field | Unknown filter field: 'secret'. Allowed fields: [name, category] |
| Invalid operator for field | Operator 'like' is not allowed for field 'active'. Allowed: [EQ] |
| Unknown operator | Unknown filter operator: 'contains'. Supported: eq, neq, like, ... |
| Invalid boolean value | Invalid boolean value: 'banana'. Use 'true' or 'false' |
| Invalid datetime value | Invalid value '2025-13-01' for type LocalDateTime |
| Unknown sort field | Unknown sort field: 'secret'. Allowed fields: [name, created] |
| Invalid sort direction | Invalid sort direction: 'up'. Use 'asc' or 'desc' |
To map these to HTTP 400 responses, add an exception handler in your application:
@RestControllerAdvice
public class FilterExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Mono<Map<String, String>> handleFilterError(IllegalArgumentException ex) {
return Mono.just(Map.of("error", ex.getMessage()));
}
}The library is designed to be injection-safe:
- Field whitelist -- only fields declared in
EntityFilterSpeccan be filtered or sorted - Operator whitelist -- each field defines which operators are allowed
- Type conversion -- values are converted to typed Java objects before being passed to MongoDB
- Regex escaping -- the
likeoperator usesPattern.quote()to escape user input - No dot notation -- field name regex
\w+prevents access to nested fields likecredentials.hash - Parameterized queries -- Spring Data's Criteria API uses BSON encoding, not string concatenation
git clone https://github.com/magicthings/spring-reactive-mongo-filter.git
cd spring-reactive-mongo-filter
./mvnw clean installRequires Java 21+.
Contributions are welcome! Please open an issue or submit a pull request.
This project is licensed under the MIT License.