Skip to content

Repository files navigation

jsonb-jpa-toolkit

CI

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

Features

  • 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 JsonDialect SPI — add support for other databases without forking

Requirements

  • Java 17+
  • Spring Boot 3.2+
  • Hibernate 6.4+
  • PostgreSQL 12+ (SQL/JSON path functions)

Installation

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.


Quick Start

1. Declare an entity with a JSONB column

@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
}

2. Extend JpaSpecificationExecutor on your repository

public interface ProductRepository
        extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {
}

3. Build queries with JsonSpecBuilder

// 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)");

API Reference

JsonSpecBuilder methods

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

JsonType

OBJECT, ARRAY, STRING, NUMBER, BOOLEAN, NULL — mirrors jsonb_typeof return values.

Composition

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

Extending to other databases

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:

  1. Implement io.github.jsonbjpatoolkit.core.JsonDialect.
  2. Register your implementation either as a Spring @Bean of type JsonDialect (the starter's auto-configuration backs off when one is already present) or by calling JsonDialectResolver.setDialect(yourDialect) at startup.
  3. If you need custom SQL functions, contribute them via Hibernate's FunctionContributor SPI — see PostgresJsonbFunctionContributor in jsonb-jpa-toolkit-postgresql as a reference.

Modules

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

Build & Test

mvn clean install

Integration 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 -am

License

Apache License 2.0

About

Type-safe, fluent API to query PostgreSQL JSONB columns via JPA Specifications and Spring Data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages