Library for building and running advanced and dynamic queries using JPA in Spring Boot.
Building dynamic and type-safe queries with JPA can quickly become complex and repetitive.
JPA Search Helper simplifies this process by providing a fluent, declarative way to build advanced search filters and projections, with minimal boilerplate.
With this library, you can:
- Dynamically build queries using simple request parameters or structured JSON input.
- Combine filters with logical operators (AND, OR, NOT), without writing custom Specification or CriteriaBuilder code.
- Reuse your existing JPA entities or domain models through simple annotations.
- Support projections to return only specific fields instead of entire entities.
- Keep full control of the repository layer, without exposing new endpoints.
In short, it helps you build powerful, flexible search APIs in Spring Boot, with zero query code.
| Type | Status |
|---|---|
| Build (CI) | |
| CodeQL | |
| Artifact | |
| Javadocs | |
| OpenSSF Score | |
| OpenSSF Best Practices | |
| Dependabot |
Map<String, String>, to support GET endpoints with query params.With jpa-search-helper your controller* can handle requests like this:
Mode 1:
curl -X GET \
'https://myexampledomain.com/persons?
firstName=Biagio
&lastName_startsWith=Toz
&birthDate_gte=19910101
&country_in=IT,FR,DE
&address_eq=Via Roma 1,Via Milano/,1,20 West/,34th Street
&company.name_in=Microsoft,Apple,Google
&company.employees_between=500,5000'
Mode 2:
curl -X POST -H "Content-type: application/json" -d '{
"filter" : {
"operator": "and", // the first filter must contain a root operator: AND, OR or NOT
"filters" : [
{
"operator": "eq",
"key": "firstName",
"value": "Biagio"
},
{
"operator": "or",
"filters": [
{
"operator": "startsWith",
"key": "lastName",
"value": "Toz",
"options": {
"ignoreCase": true
}
},
{
"operator": "endsWith",
"key": "lastName",
"value": "ZZI",
"options": {
"ignoreCase": true,
"trim" : true
}
}
]
},
{
"operator": "in",
"key": "company.name",
"values": ["Microsoft", "Apple", "Google"]
},
{
"operator": "or",
"filters": [
{
"operator": "gte",
"key": "birthDate",
"value": "19910101"
},
{
"operator": "lte",
"key": "birthDate",
"value": "20010101"
}
]
},
{
"operator": "between",
"key" : "company.employees",
"values": [500, 5000],
"options": {
"negate": true
}
}
]
},
"options": {
"pageSize": 10,
"pageOffset": 0,
"sortOptions" : [
{
"key": "birthDate",
"desc": false
}
]
}
}' 'https://myexampledomain.com/persons'
..how can you do it? Keep reading this README!
* Please note: the library itself does not expose any controllers or HTTP endpoints, it only provides the repository layer responsible for building and executing the queries.
| JPA Search Helper | Spring Boot | JDK |
|---|---|---|
| [v0.0.1 - v2.1.1] | 3.2.x | [17 - 25] |
| [v3.0.0 - v3.2.2] | 3.3.x | [17 - 25] |
| [v3.3.0 - v3.4.5] | 3.4.x | [17 - 25] |
| [v3.5.0 - v3.5.5] | 3.5.x | [17 - 25] |
| [v3.6.0 - v3.6.4] | 4.x.x | [17 - 25] |
| [v4.0.0 - latest] | 4.x.x | [17 - 25] |
<dependency>
<groupId>app.tozzi</groupId>
<artifactId>jpa-search-helper</artifactId>
<version>4.1.0</version>
</dependency>
implementation 'app.tozzi:jpa-search-helper:4.1.0'
@Searchable annotationStart by applying the @Searchable annotation to the fields in your Domain Model, or alternatively your JPA entity, that you want to make available for search.
If you have fields that you want to make searchable within other objects then annotate these with @NestedSearchable.
@Data
public class Person {
@Searchable
private String firstName;
@Searchable
private String lastName;
@Searchable(entityFieldKey = "dateOfBirth")
private Date birthDate;
@Searchable
private String country;
@Searchable
private String fillerOne;
@Searchable
private String fillerTwo;
@NestedSearchable
private Company company;
@Searchable(targetType = JPASearchType.JSONB, entityFieldKey = "data", jsonPath = "address.street")
private String addressStreet;
@Searchable(targetType = JPASearchType.JSONB, entityFieldKey = "data", jsonPath = "address.city")
private String addressCity;
@Data
public static class Company {
@Searchable(entityFieldKey= "companyEntity.name")
private String name;
@Searchable(entityFieldKey= "companyEntity.employeesCount")
private int employees;
}
}
The annotation allows you to specify:
Core properties:
entityFieldKey:
the name of the field defined on the entity bean (not to be specified if using the annotation on the entity bean). If not specified the key will be the field name.
targetType: the managed object type by entity. If not specified the librariy tries to obtain it based on field type (es. Integer field without target type definition will be INTEGER). If there is no type compatible with those managed, it will be managed as a string. Managed types:
STRING, INTEGER, DOUBLE, FLOAT, LONG, BIGDECIMAL, BOOLEAN, DATE, LOCALDATE, LOCALDATETIME, LOCALTIME, OFFSETDATETIME, OFFSETTIME, ZONEDDATETIME, ENUM, DATE_SQL, TIME_SQL, INSTANT, TIMESTAMP, UUID, JSONBValidation properties:
datePattern: only for DATE, LOCALDATE, LOCALDATETIME, LOCALTIME, OFFSETDATETIME, OFFSETTIME, ZONEDDATETIME, TIMESTAMP, TIME_SQL, DATE_SQL, INSTANT target types. Defines the date pattern to use.
maxSize, minSize: maximum/minimum length of the value (for STRING types).maxDigits, minDigits: only for numeric types. Maximum/minimum number of digits.regexPattern: regex pattern.decimalFormat: only for decimal numeric types. Default #.##
Other:
sortable: if false, the field can be used by search but cannot be used for sorting. Default: true.trim: apply trim.tags: useful if the Domain Model field can correspond to multiple entity fields (the example is available further down).allowedFilters: exclusively allowed filters.notAllowedFilters: not allowed filters.likeFilters: allowed like filters (contains, startsWith, endsWith). Default: true.ordinalEnum: only for ENUM type; true if search via ordinaljsonPath: required for JSONB; represents the JSON path inside the jsonb column.elementCollection: set to true when the underlying entity attribute is annotated with @ElementCollection (e.g. List<String>). EQ and IN are evaluated with cb.isMember(...); other operators fall back to a LEFT JOIN on the collection table.Continuing the example, here are our entity classes:
@Entity
@Data
public class PersonEntity {
@Id
private Long id;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Column(name = "BIRTH_DATE")
private Date dateOfBirth;
@Column(name = "COUNTRY")
private String country;
@Column(name = "FIL_ONE")
private String fillerOne;
@Column(name = "FIL_TWO")
private String fillerTwo;
@Type(JsonType.class)
@Column(name = "DATA_ADDRESS", columnDefinition = "jsonb")
private Map<String, Object> data;
@OneToOne
private CompanyEntity companyEntity;
}
@Entity
@Data
public class CompanyEntity {
@Id
private Long id;
@Column(name = "NAME")
private String name;
@Column(name = "COUNT")
private Integer employeesCount;
}
JPASearchRepository interfaceYour Spring JPA repository must extend JPASearchRepository<YourEntityClass>.
@Repository
public interface PersonRepository extends JpaRepository<PersonEntity, Long>, JPASearchRepository<PersonEntity> {
}
In your manager, or in your service, or wherever you want to use the repository:
Mode 1: define a map :
// ...
@Autowired
private PersonRepository personRepository;
public List<Person> advancedSearch() {
// This is a simple example; in a real use case, these filters would typically be passed directly from the controller.
Map<String, String> filters = new HashMap<>();
filters.put("firstName_eq", "Biagio");
filters.put("lastName_startsWith#i", "Toz"); // ignore case
filters.put("birthDate_gte", "19910101");
filters.put("country_in", "IT,FR,DE");
filters.put("company.name_eq#n", "Bad Company"); // negation
filters.put("company.employees_between", "500,5000");
filters.put("fillerOne_null#n", "true"); // not null
filters.put("fillerTwo_empty", "true"); // empty
// Without pagination
List<PersonEntity> fullSearch = personRepository.findAll(filters, Person.class);
filters.put("birthDate_sort", "ASC"); // sorting key and sorting order
filters.put("_limit", "10"); // page size
filters.put("_offset", "0"); // page offset
// With pagination
Page<PersonEntity> sortedAndPaginatedSearch = personRepository.findAllWithPaginationAndSorting(filters, Person.class);
// ...
}
// ...
Mode 2: instead of a map, you will need to use JPASearchInput, shown here, for simplicity, in JSON format.
{
"filter" : {
"operator": "and", // the first filter must contain a root operator: AND, OR or NOT
"filters" : [
{
"operator": "eq",
"key": "firstName",
"value": "Biagio"
},
{
"operator": "or",
"filters": [
{
"operator": "startsWith",
"key": "lastName",
"value": "Toz",
"options": {
"ignoreCase": true
}
},
{
"operator": "endsWith",
"key": "lastName",
"value": "ZZI",
"options": {
"ignoreCase": true,
"trim" : true
}
}
]
},
{
"operator": "in",
"key": "company.name",
"values": ["Microsoft", "Apple", "Google"]
},
{
"operator": "or",
"filters": [
{
"operator": "gte",
"key": "birthDate",
"value": "19910101"
},
{
"operator": "lte",
"key": "birthDate",
"value": "20010101"
}
]
},
{
"operator": "empty",
"key": "fillerOne",
"options": {
"negate": true
}
},
{
"operator": "between",
"key" : "company.employees",
"values": [500, 5000],
"options": {
"negate": true
}
}
]
},
"options": {
"pageSize": 10,
"pageOffset": 0,
"sortOptions" : [
{
"key": "birthDate",
"desc": false
}
]
}
}
Through Mode 2 it is possible to manage complex filters with AND, OR and NOT (see later).
$ claude mcp add jpa-search-helper \
-- python -m otcore.mcp_server <graph>