MCPcopy Index your code
hub / github.com/biagioT/jpa-search-helper

github.com/biagioT/jpa-search-helper @4.1.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release 4.1.0 ↗ · + Follow
393 symbols 1,330 edges 70 files 12 documented · 3% updated 8d ago4.1.0 · 2026-06-12★ 62
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

JPA Search Helper

Library for building and running advanced and dynamic queries using JPA in Spring Boot.

Why use JPA Search Helper?

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.

Status

Type Status
Build (CI) Build (github)
CodeQL Build (github)
Artifact Maven Central
Javadocs javadoc
OpenSSF Score OpenSSF Scorecard
OpenSSF Best Practices OpenSSF Best Practices
Dependabot Dependabot

TL;DR

Key features

  • Queries: the library supports two modes for building advanced and dynamic queries:
  • Mode 1: Via Map<String, String>, to support GET endpoints with query params.
  • Mode 2: Via an object, to support POST endpoints that expect query parameters in the body (from 2.0.0 version)
  • Projection: for both modes, the library allows you to extract only a subselection of fields from the query (from 3.2.0 version)

Spoiler

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.

Compatibility Matrix

Minimum requirements

  • Java 17 or later
  • Spring Boot 3.2.x or later
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]

Project dependency

Maven

<dependency>  
 <groupId>app.tozzi</groupId> 
 <artifactId>jpa-search-helper</artifactId> 
 <version>4.1.0</version>
</dependency>  

Gradle

implementation 'app.tozzi:jpa-search-helper:4.1.0'

Queries - Usage

1. @Searchable annotation

Start 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, JSONB
  • Validation 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 ordinal
  • jsonPath: 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;

}

2. JPASearchRepository interface

Your Spring JPA repository must extend JPASearchRepository<YourEntityClass>.

@Repository
public interface PersonRepository extends JpaRepository<PersonEntity, Long>, JPASearchRepository<PersonEntity> {

}

3. Search implementation

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).

Exceptions:

  • If a field does not exist, is not searchable or is not sortable, you wil

Extension points exported contracts — how you extend this code

JPAProjectionRepository (Interface)
Unified projection API exposed by the library. Each operation comes in four flavours: {@link JPASearchI [1 implementers]
src/main/java/app/tozzi/repository/JPAProjectionRepository.java
MyPostgresRepository (Interface)
(no doc)
src/test/java/app/tozzi/postgresql/repository/MyPostgresRepository.java
JPASearchRepository (Interface)
Unified search API exposed by the library. Every operation comes in four flavours: {@link JPASearchInpu
src/main/java/app/tozzi/repository/JPASearchRepository.java
MyRepository (Interface)
(no doc)
src/test/java/app/tozzi/repository/MyRepository.java
JPASearchFunction (Interface)
(no doc)
src/main/java/app/tozzi/model/JPASearchFunction.java

Core symbols most depended-on inside this repo

equals
called by 233
src/main/java/app/tozzi/core/JPAProjectionProcessor.java
getValue
called by 153
src/main/java/app/tozzi/core/JPASearchCoreValueProcessor.java
processValue
called by 69
src/main/java/app/tozzi/core/JPASearchCoreValueProcessor.java
getType
called by 44
src/main/java/app/tozzi/util/ReflectionUtils.java
builder
called by 39
src/main/java/app/tozzi/model/JPASearchOptions.java
build
called by 39
src/main/java/app/tozzi/model/JPASearchOptions.java
getIdFields
called by 36
src/main/java/app/tozzi/util/ReflectionUtils.java
findAll
called by 34
src/main/java/app/tozzi/repository/JPASearchRepository.java

Shape

Method 299
Class 81
Enum 8
Interface 5

Languages

Java100%

Modules by API surface

src/test/java/app/tozzi/core/JPASearchCoreTest.java36 symbols
src/main/java/app/tozzi/util/GenericUtils.java29 symbols
src/test/java/app/tozzi/util/GenericUtilsTest.java27 symbols
src/test/java/app/tozzi/util/ValidationUtilsTest.java23 symbols
src/test/java/app/tozzi/postgresql/core/JsonbTest.java21 symbols
src/test/java/app/tozzi/core/ProjectionBugVerificationTest.java17 symbols
src/test/java/app/tozzi/core/JPAProjectionProcessorTest.java17 symbols
src/main/java/app/tozzi/core/JPASearchCoreValueProcessor.java15 symbols
src/test/java/app/tozzi/util/ReflectionUtilsTest.java14 symbols
src/main/java/app/tozzi/core/JPAProjectionProcessor.java12 symbols
src/test/java/app/tozzi/core/JPASearchCoreValueProcessorNumberParsingTest.java11 symbols
src/test/java/app/tozzi/core/JPASearchCoreFixes4Test.java11 symbols

For agents

$ claude mcp add jpa-search-helper \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page