Fluent JPA Specifications for querying PostgreSQL JSONB columns with Spring Data JPA.
Write type-safe, composable queries over JSON documents without dropping down to native SQL or string-building dynamic JPQL.
Specification<Product> spec = JsonSpecBuilder.where("metadata")
.atPath("brand")
.isEqualTo("Acme");
List<Product> products = productRepository.findAll(spec);- Fluent, type-safe API built on top of
org.springframework.data.jpa.domain.Specification - Full coverage of PostgreSQL JSONB operators:
->>,#>>,->,#>,@>,<@,?,?|,?& - SQL/JSON path support via
jsonb_path_exists - Array operations: contains, length, empty checks
- Type checks via
jsonb_typeof - Composition with standard
Specification.and/.or/.not - Spring Boot auto-configuration — zero config needed
- Extensible via a
JsonDialectSPI — add support for other databases without forking
- Java 17+
- Spring Boot 3.2+
- Hibernate 6.4+
- PostgreSQL 12+ (SQL/JSON path functions)
Add the Spring Boot starter to your pom.xml:
<dependency>
<groupId>io.github.jsonbjpatoolkit</groupId>
<artifactId>jsonb-jpa-toolkit-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>The starter transitively brings the core module and the PostgreSQL dialect. Auto-configuration registers the dialect automatically on startup.
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(columnDefinition = "jsonb")
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> metadata;
// getters / setters
}public interface ProductRepository
extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {
}// Extract a value and compare
Specification<Product> brandAcme = JsonSpecBuilder.where("metadata")
.atPath("brand").isEqualTo("Acme");
Specification<Product> cheapPrice = JsonSpecBuilder.where("metadata")
.atPath("price").lessThan(50);
productRepository.findAll(brandAcme.and(cheapPrice));
// Containment
Specification<Product> premium = JsonSpecBuilder.where("metadata")
.contains(Map.of("tier", "premium"));
// Containment (single key shorthand)
Specification<Product> acme = JsonSpecBuilder.where("metadata")
.containsEntry("brand", "Acme");
// Key existence
Specification<Product> hasDiscount = JsonSpecBuilder.where("metadata")
.hasKey("discount");
// Array contents
Specification<Product> tagged = JsonSpecBuilder.where("metadata")
.atPath("tags").arrayContains("sale");
// SQL/JSON path
Specification<Product> highlyRated = JsonSpecBuilder.where("metadata")
.matchesPath("$.ratings[*] ? (@ > 4)");| Method | SQL equivalent |
|---|---|
.where(String fieldName) |
Entry point — target the given JSONB column |
.atPath(String jsonPath) |
Navigate to a path (e.g. "address.city") |
.isEqualTo(String / Number / Boolean) |
(column ->> 'key')[::cast] = ? |
.isNotEqualTo(String / Number) |
(column ->> 'key')[::cast] <> ? |
.greaterThan(Number) |
(column ->> 'key')::numeric > ? |
.greaterThanOrEqual(Number) |
>= ? |
.lessThan(Number) |
< ? |
.lessThanOrEqual(Number) |
<= ? |
.between(Number low, Number high) |
BETWEEN ? AND ? |
.like(String pattern) |
column ->> 'key' LIKE ? |
.ilike(String pattern) |
case-insensitive LIKE |
.in(String... / Number...) |
IN (...) |
.isNull() |
missing key OR JSON null |
.isNotNull() |
present AND not JSON null |
.contains(Map<String, ?> json) |
column @> fragment::jsonb |
.containsEntry(String key, Object value) |
column @> '{"key":"value"}'::jsonb |
.isContainedIn(Map<String, ?> json) |
column <@ fragment::jsonb |
.hasKey(String key) |
column ? 'key' |
.hasAnyKey(String... keys) |
column ?| array[...] |
.hasAllKeys(String... keys) |
column ?& array[...] |
.arrayContains(Object value) |
column -> 'key' @> '"value"'::jsonb |
.arrayLengthIs(int length) |
jsonb_array_length(column -> 'key') = ? |
.arrayIsEmpty() / .arrayIsNotEmpty() |
length = 0 / > 0 |
.matchesPath(String sqlJsonPath) |
jsonb_path_exists(column, '...') |
.hasType(JsonType type) |
jsonb_typeof(column) = '...' |
OBJECT, ARRAY, STRING, NUMBER, BOOLEAN, NULL — mirrors jsonb_typeof return values.
Each terminal method returns a Specification<T> where the entity type is inferred from the assignment target. Combine them using the standard Spring Data API:
Specification<Product> brandAcme = JsonSpecBuilder.where("metadata")
.atPath("brand").isEqualTo("Acme");
Specification<Product> priceOver50 = JsonSpecBuilder.where("metadata")
.atPath("price").greaterThan(50);
Specification<Product> brandBeta = JsonSpecBuilder.where("metadata")
.atPath("brand").isEqualTo("Beta");
// AND / OR / NOT
productRepository.findAll(brandAcme.and(priceOver50));
productRepository.findAll(brandAcme.or(brandBeta));
productRepository.findAll(Specification.not(brandAcme));
// Chain them together
productRepository.findAll(brandAcme.and(priceOver50).or(Specification.not(brandBeta)));The JsonDialect SPI in jsonb-jpa-toolkit-core defines the contract every database adapter must implement (extraction, containment, key existence, path matching, type check, array operations, casting). To add support for another database:
- Implement
io.github.jsonbjpatoolkit.core.JsonDialect. - Register your implementation either as a Spring
@Beanof typeJsonDialect(the starter's auto-configuration backs off when one is already present) or by callingJsonDialectResolver.setDialect(yourDialect)at startup. - If you need custom SQL functions, contribute them via Hibernate's
FunctionContributorSPI — seePostgresJsonbFunctionContributorinjsonb-jpa-toolkit-postgresqlas a reference.
| Module | Purpose |
|---|---|
jsonb-jpa-toolkit-core |
Fluent API (JsonSpecBuilder) and JsonDialect SPI |
jsonb-jpa-toolkit-postgresql |
PostgreSQL implementation of JsonDialect + Hibernate function contributor |
jsonb-jpa-toolkit-spring-boot-starter |
Spring Boot auto-configuration |
jsonb-jpa-toolkit-tests |
Integration tests against a real PostgreSQL via Testcontainers |
mvn clean installIntegration tests spin up a PostgreSQL 16 container via Testcontainers, so a running Docker daemon is required. The tests are self-contained — no local database setup needed.
mvn test -pl jsonb-jpa-toolkit-tests -amApache License 2.0