spring-web-query is split into two artifacts:
spring-web-query-core: Core annotations, argument resolvers, validation, and query utilities.spring-boot-starter-web-query: Spring Boot auto-configuration on top ofcorefor zero-config setup.
- Secure filtering: Whitelist filterable fields and specific operators using
@RsqlFilterable. - Restricted sorting: Allow sorting only on fields explicitly marked with
@Sortable. - Deep path resolution: Support for nested properties (for example
user.address.city), collections, and arrays. - API aliasing: Use
@FieldMappingto expose clean API field names without leaking internal entity structures. - Zero-config (starter): Auto-configures argument resolvers for
@RsqlSpecand@RestrictedPageable. - DoS protection: Built-in maximum page size enforcement.
- ISO-8601 ready: Handling of date/time formats in query strings.
Use one of the following depending on your setup.
<dependency>
<groupId>in.co.akshitbansal</groupId>
<artifactId>spring-boot-starter-web-query</artifactId>
<version>X.X.X</version>
</dependency>This includes spring-web-query-core transitively and auto-registers required configuration.
<dependency>
<groupId>in.co.akshitbansal</groupId>
<artifactId>spring-web-query-core</artifactId>
<version>X.X.X</version>
</dependency>Use this when you do not want Boot starter auto-configuration and prefer manual resolver setup.
The project targets Spring Boot 4.0.2+ and Java 21+.
mainalways contains-SNAPSHOTversions.- Every commit to
mainpublishes a snapshot version to Maven Central. - Releases are created from
release/**branches: versions are changed to non-snapshot values, and release publishing is triggered manually through a GitHub Action. - For all non-
mainbranch commits and pull requests, CI only verifies build and test success.
@Entity
public class User {
@RsqlFilterable(operators = {RsqlOperator.EQUAL, RsqlOperator.IN})
private String status;
@RsqlFilterable(operators = {RsqlOperator.GREATER_THAN, RsqlOperator.LESS_THAN})
private Instant createdAt;
@Sortable
@RsqlFilterable(operators = {RsqlOperator.EQUAL})
private String username;
@OneToOne
private Profile profile;
}
@Entity
public class Profile {
@RsqlFilterable(operators = {RsqlOperator.EQUAL})
private String city;
}@GetMapping("/users")
@WebQuery(entityClass = User.class)
public Page<User> search(
@RsqlSpec Specification<User> spec,
@RestrictedPageable Pageable pageable
) {
return userRepository.findAll(spec, pageable);
}@WebQuery is required on the controller method when using @RsqlSpec and/or @RestrictedPageable.
| Feature | Query |
|---|---|
| Simple Filter | /users?filter=status==ACTIVE |
| Complex Logical | /users?filter=status==ACTIVE;username==john* |
| Date Range | /users?filter=createdAt=gt=2024-01-01T00:00:00Z |
| Nested Paths | /users?filter=profile.city==NewYork |
| Secure Sorting | /users?sort=username,asc |
@WebQuery(
entityClass = User.class,
fieldMappings = {
@FieldMapping(name = "joined", field = "createdAt", allowOriginalFieldName = false)
}
)
public Page<User> search(
@RsqlSpec Specification<User> spec,
@RestrictedPageable Pageable pageable
) {
return userRepository.findAll(spec, pageable);
}name: The alias to be used in the query.field: The actual entity field path.allowOriginalFieldName: Iftrue, both the alias and original field name can be used. Iffalse(default), only the alias is allowed for both filtering and sorting.
Query: /users?filter=joined=gt=2024-01-01T00:00:00Z
Sort query: /users?sort=joined,desc
You can define custom operators to extend filtering capabilities.
public class IsMondayOperator implements RsqlCustomOperator<Long> {
@Override
public ComparisonOperator getComparisonOperator() {
return new ComparisonOperator("=monday=", Arity.nary(0));
}
@Override
public Class<Long> getType() {
return Long.class;
}
@Override
public Predicate toPredicate(RSQLCustomPredicateInput input) {
CriteriaBuilder cb = input.getCriteriaBuilder();
// MySQL example: DAYOFWEEK() returns 1 for Sunday, 2 for Monday...
return cb.equal(
cb.function("DAYOFWEEK", Long.class, input.getPath()),
2
);
}
}Register your custom operators as a Spring Bean. You can register multiple RsqlCustomOperatorsConfigurer beans, and the library will automatically combine all custom operators from all registered configurers.
@Configuration
public class RsqlConfig {
@Bean
public RsqlCustomOperatorsConfigurer customOperators() {
return () -> Set.of(new IsMondayOperator());
}
}Whitelisting is required for custom operators just like default ones.
@Entity
public class User {
@RsqlFilterable(
operators = {RsqlOperator.EQUAL},
customOperators = {IsMondayOperator.class}
)
private LocalDateTime createdAt;
}Query: /users?filter=createdAt=monday=
Configure maximum allowed page size in application.properties (default 100):
api.pagination.max-page-size=500The library provides a hierarchy of exceptions to distinguish between client-side validation errors and developer-side configuration issues. All exceptions extend the base QueryException.
QueryValidationException: Thrown when an API consumer provides invalid input. These should typically be returned as a400 Bad Request.- Filtering on a non-
@RsqlFilterablefield. - Using a disallowed operator on a field.
- Sorting on a non-
@Sortablefield. - Using an original field name when a mapping alias is required (
allowOriginalFieldName = false). - Malformed RSQL syntax.
- Filtering on a non-
QueryConfigurationException: Thrown when the library or entity mapping is misconfigured by the developer. These should typically be treated as a500 Internal Server Error.- Missing
@WebQueryon a controller method that uses@RsqlSpecor@RestrictedPageable. - Custom operators referenced in
@RsqlFilterablethat are not registered. - Field mappings pointing to non-existent fields on the entity.
- Missing
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(QueryValidationException.class)
public ResponseEntity<String> handleValidationException(QueryValidationException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
@ExceptionHandler(QueryConfigurationException.class)
public ResponseEntity<String> handleConfigurationException(QueryConfigurationException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Internal configuration error");
}
// Alternatively, catch the base exception for unified handling
@ExceptionHandler(QueryException.class)
public ResponseEntity<String> handleQueryException(QueryException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
}- Parsing: The RSQL string is parsed into an AST.
- Validation: A custom
RSQLVisitortraverses the AST and checks every node against the@RsqlFilterableconfiguration on the target entity defined by@WebQuery. - Reflection:
ReflectionUtilresolves dot-notation paths, handling JPA associations and collection types. - Specification: Once validated, it is converted into a
Specification<T>compatible with Spring Data JPAfindAll(Specification, Pageable).
Licensed under the Apache License, Version 2.0.
You may obtain a copy of the License at:
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.