SpringBoot
JPA
PostgreSQL
JSONB
DataStorage

how to store PostgreSQL jsonb using SpringBoot JPA?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

PostgreSQL jsonb is a good fit when part of your data is flexible but still needs indexing and query support. In a modern Spring Boot application using Hibernate 6, the cleanest mapping is usually a normal entity field annotated for JSON and backed by a PostgreSQL jsonb column.

Define the jsonb Column in PostgreSQL

Start with a table that uses jsonb explicitly:

sql
1CREATE TABLE orders (
2    id BIGSERIAL PRIMARY KEY,
3    status VARCHAR(50) NOT NULL,
4    attributes JSONB NOT NULL
5);

Using jsonb instead of plain json gives you better indexing and richer operator support in PostgreSQL.

Map JSON with Hibernate 6

In current Spring Boot setups that use Hibernate 6, you can map JSON with @JdbcTypeCode:

java
1import com.fasterxml.jackson.databind.JsonNode;
2import jakarta.persistence.Column;
3import jakarta.persistence.Entity;
4import jakarta.persistence.GeneratedValue;
5import jakarta.persistence.GenerationType;
6import jakarta.persistence.Id;
7import org.hibernate.annotations.JdbcTypeCode;
8import org.hibernate.type.SqlTypes;
9
10@Entity
11public class OrderEntity {
12
13    @Id
14    @GeneratedValue(strategy = GenerationType.IDENTITY)
15    private Long id;
16
17    private String status;
18
19    @JdbcTypeCode(SqlTypes.JSON)
20    @Column(columnDefinition = "jsonb")
21    private JsonNode attributes;
22
23    public Long getId() { return id; }
24    public String getStatus() { return status; }
25    public void setStatus(String status) { this.status = status; }
26    public JsonNode getAttributes() { return attributes; }
27    public void setAttributes(JsonNode attributes) { this.attributes = attributes; }
28}

JsonNode is a good choice when the structure is flexible. If the JSON shape is stable, a dedicated Java class can be even better.

Save and Load the Entity

A regular Spring Data JPA repository works as expected:

java
1import org.springframework.data.jpa.repository.JpaRepository;
2
3public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
4}

Example usage:

java
1import com.fasterxml.jackson.databind.ObjectMapper;
2import org.springframework.stereotype.Service;
3
4@Service
5public class OrderService {
6    private final OrderRepository repository;
7    private final ObjectMapper objectMapper;
8
9    public OrderService(OrderRepository repository, ObjectMapper objectMapper) {
10        this.repository = repository;
11        this.objectMapper = objectMapper;
12    }
13
14    public OrderEntity create() {
15        OrderEntity order = new OrderEntity();
16        order.setStatus("NEW");
17        order.setAttributes(objectMapper.createObjectNode()
18            .put("source", "mobile")
19            .put("priority", "high"));
20        return repository.save(order);
21    }
22}

From the application side, it behaves like any other mapped field.

Querying jsonb

JPA itself does not provide first-class abstractions for every PostgreSQL JSON operator, so native queries are often the pragmatic choice:

java
1import java.util.List;
2import org.springframework.data.jpa.repository.JpaRepository;
3import org.springframework.data.jpa.repository.Query;
4
5public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
6
7    @Query(value = """
8        select * from orders
9        where attributes ->> 'source' = :source
10        """, nativeQuery = true)
11    List<OrderEntity> findBySource(String source);
12}

That gives you access to PostgreSQL’s jsonb operators without forcing awkward application-side filtering.

Index the JSON Paths You Query

If you query inside JSON regularly, index for it. A broad GIN index can help:

sql
CREATE INDEX idx_orders_attributes ON orders USING GIN (attributes);

For specific paths, expression indexes can be better:

sql
CREATE INDEX idx_orders_source ON orders ((attributes ->> 'source'));

Without indexing, jsonb can become convenient to write but expensive to search.

When to Avoid jsonb

jsonb is useful, but it is not a license to stop modeling data. If a field is required, heavily queried, and structurally stable, a normal relational column is often better. jsonb is strongest where:

  • shape varies legitimately
  • partial semi-structured attributes exist
  • the document changes over time

Treat it as a complement to the relational model, not a full replacement for it.

Common Pitfalls

  • Storing stable relational fields in jsonb just because it feels flexible.
  • Forgetting columnDefinition = "jsonb" and ending up with the wrong database type.
  • Assuming JPQL will cover every PostgreSQL JSON operator cleanly.
  • Skipping indexes on JSON paths that are frequently queried.
  • Using an untyped JSON blob everywhere when part of the structure is actually stable enough for a real Java model.

Summary

  • Use a real PostgreSQL jsonb column when semi-structured data belongs in the database.
  • In Hibernate 6, @JdbcTypeCode(SqlTypes.JSON) is the modern mapping approach.
  • Map flexible payloads as JsonNode or a dedicated Java type, depending on how stable the schema is.
  • Use native PostgreSQL JSON operators when querying inside the document.
  • Add the right indexes so jsonb remains operationally useful, not just convenient to store.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.