Skip to main content

Command Palette

Search for a command to run...

How to Create a Custom PostgreSQL Dialect (Hibernate 6) and Make Spring Boot Recognize It

You don’t start by writing a “custom dialect” because it sounds cool. You start because your SQL is smarter than Hibernate’s defaults: you need jsonb to be the canonical JSON column, you want correct DDL for uuid, maybe inet, and you’d like nativ...

Published
8 min read
How to Create a Custom PostgreSQL Dialect (Hibernate 6) and Make Spring Boot Recognize It
T

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.

1. The idea in one sitting: why a custom PostgreSQL Dialect is still useful in 2025

Hibernate 6 knows about PostgreSQL out of the box. However, “knowing” is not “perfectly tuned for your schema.” You may need to:

  • Make jsonb the default for JSON, not a generic other type.
  • Ensure array types (text[], uuid[]) DDL renders correctly.
  • Advertise native functions to the query engine for better JPQL/Criteria support.
  • Normalize types like uuid, inet, citext, or ltree so they don’t fall back to OTHER.

Spring Boot will try to infer a dialect from JDBC metadata (and it’s usually fine). But when you add custom type declarations and function support, you want a bespoke Dialect class and you want Boot to use it every time. For context on Boot’s decision points (“let Hibernate figure it out” vs setting a platform explicitly), the official Boot docs call out the spring.jpa.database-platform property for this exact purpose. (Home)

1.1 The smallest useful custom Dialect for PostgreSQL (Hibernate 6)

In Hibernate 6, a Dialect contributes type mappings in its constructor and (optionally) functions via the function registry. Keep it small; the point is predictable DDL and sane defaults.

package com.example.persistence.dialect;

import org.hibernate.dialect.PostgreSQLDialect;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.type.SqlTypes;

public class PgJsonbDialect extends PostgreSQLDialect {

public PgJsonbDialect() {
// Pin a minimum server version if you rely on modern JSON operators
super(DatabaseVersion.make(14)); // or 15/16 depending on production

// Tell Hibernate that SqlTypes.JSON should be rendered as jsonb
registerColumnType(SqlTypes.JSON, "jsonb");

// Optional: make sure other Postgres-specific types render properly
registerColumnType(SqlTypes.INET, "inet");
registerColumnType(SqlTypes.UUID, "uuid");

// If you use arrays explicitly via @JdbcTypeCode(SqlTypes.ARRAY)
// you can set a default; in practice we map arrays at column level.
}

// If you need built-in function patterns, you can contribute them by
// overriding initializeFunctionRegistry(...) in Hibernate 6:
// @Override
// public void initializeFunctionRegistry(FunctionContributions fc) {
// super.initializeFunctionRegistry(fc);
// var reg = fc.getFunctionRegistry();
// // Example: JSON text extraction shortcut
// // reg.registerPattern("jsonb_extract_text", "(?1 #>> ?2)", StandardBasicTypes.STRING);
// }
}

This class does three important things. First, it pins a server version, which helps Hibernate emit the right SQL features. Second, it declares that SqlTypes.JSON corresponds to jsonb when generating DDL. Third, it leaves room to declare functions the query engine can understand (handy when you’d like to use jsonb operators without native SQL). For JSON/JSONB operators and behavior, PostgreSQL’s own docs are definitive. (PostgreSQL)

To visualize how the Dialect influences SQL generation vs the ORM layer, peek at a high-level architecture diagram; it helps to see Dialect on the SQL-generation side of the house. (tutorialspoint.com)

1.2 Teaching Spring Boot to actually use your dialect

Spring Boot will not guess your custom class. You must set one property:

# application.properties
spring.jpa.database-platform=com.example.persistence.dialect.PgJsonbDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

This is the canonical way to make Boot pick a specific dialect. When you don’t set this, Boot will default to “let Hibernate detect from JDBC,” which is fine for basics but won’t include your overrides. The Boot reference explains this switch explicitly. (Home)

1.3 Proving it works with a JSONB entity and repository

Now let’s map a field to jsonb and prove that DDL is emitted as jsonb, not text/other.

package com.example.movies;

import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

import java.util.UUID;

@Entity
@Table(name = "movie")
public class Movie {

@Id
@GeneratedValue
private UUID id;

private String title;

@Column(columnDefinition = "jsonb") // ensure DDL renders JSONB
@JdbcTypeCode(SqlTypes.JSON) // tell Hibernate this is JSON semantically
private JsonNode metadata; // Jackson on classpath is auto-detected

// getters/setters
}

package com.example.movies;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.UUID;

public interface MovieRepository extends JpaRepository<movie, UUID> { }

package com.example.movies;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class DemoData {

@Bean
CommandLineRunner seed(MovieRepository repo, ObjectMapper mapper) {
return args -> {
ObjectNode json = mapper.createObjectNode()
.put("year", 2025)
.put("rating", 8)
.putObject("cast").put("lead", "Ada");

Movie m = new Movie();
m.setTitle("The JSON Identity");
m.setMetadata(json);
repo.save(m);
};
}
}

With spring.jpa.show-sql=true, the table creation shows metadata jsonb. At runtime Hibernate 6 serializes/deserializes JSON via the configured JSON library; @JdbcTypeCode(SqlTypes.JSON) is the key that flips the mapping into JSON mode (not just “some object as bytes”). For a concise primer on this Hibernate 6 feature, see these explainers. (Thorben Janssen)

If you want a “picture worth a thousand words” about JSON vs JSONB, the Postgres docs and a JSON-B focused article provide context and visual aids. (PostgreSQL)

2. Beyond the minimum: functions, arrays, UUIDs, and tests that catch regressions

Once the basics are stable, you can lean into Postgres features without diving into native queries every other line. This is where a slightly richer Dialect pays for itself.

2.1 Enabling Postgres JSON operators and functions

Hibernate 6 revamped custom function registration. The modern way is to contribute functions inside the Dialect (or via a MetadataBuilderContributor). If you need a function like #>> (text extraction) accessible from JPQL, register a pattern. As APIs evolve, check a Hibernate-6-specific reference on custom functions and migration notes—these highlight the differences from 5.x and show examples. (aregall.tech)

You’ll still reach for native queries when you’re deep into JSON path operators, but pushing common primitives into the Dialect makes your repositories cleaner. For JSON operators themselves, Postgres’s official function/operator table is the source of truth and includes diagrams and examples you can mirror in tests. (PostgreSQL)

2.2 Arrays and UUIDs the sensible way

Modern Hibernate maps java.util.UUID directly to Postgres uuid—no custom type necessary. If you see stringly typed hacks, they’re likely leftovers from Hibernate 5.x. When in doubt, keep it idiomatic:

@Id
@GeneratedValue
private java.util.UUID id; // renders as uuid, no extra @Type needed

For arrays, the most reliable path is declaring concrete column definitions (text[], uuid[]) and mapping with @JdbcTypeCode(SqlTypes.ARRAY) plus an appropriate @Column(columnDefinition = "uuid[]"). Behavior can vary across minor versions, so pin Hibernate and test DDL output in CI.

For a quick sanity check on UUID handling history, here’s a short Q&A thread; use it as a breadcrumb, not gospel. (Stack Overflow)

2.3 A micro-test to assert the chosen Dialect (no guesswork)

A one-liner test saves you from “it works on my laptop because the profile was different.” This prints the effective Dialect at runtime.

package com.example;

import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import jakarta.persistence.EntityManagerFactory;

@SpringBootTest
class DialectSmokeTest {

@Autowired EntityManagerFactory emf;

@Test
void shouldLogDialect() {
var sfi = emf.unwrap(SessionFactoryImplementor.class);
System.out.println(">> Effective Dialect = " + sfi.getJdbcServices().getDialect().getClass().getName());
// Optionally assert:
assert sfi.getJdbcServices().getDialect().getClass().getName()
.equals("com.example.persistence.dialect.PgJsonbDialect");
}
}

If this ever prints org.hibernate.dialect.PostgreSQLDialect, you know exactly where to look: the Spring profile, the property file, or the classpath.

2.4 What about just using annotations without a custom Dialect?

You can do:

@Column(columnDefinition = "jsonb")
@JdbcTypeCode(SqlTypes.JSON)
JsonNode metadata;

and stop there. For many teams, that’s enough. But once you want arrays, inet, or function registration to be declarative and reusable, a custom Dialect keeps schema rules in one place and prevents “annotation soup.” If you’re curious how people use Hibernate 6’s JSON mapping in the wild, skim a focused tutorial or an example project for additional patterns. (Baeldung on Kotlin)

2.5 Debugging “Why isn’t my Dialect applied?”

If your logs say “Unable to determine Dialect without JDBC metadata,” Hibernate couldn’t read metadata at bootstrap (common in tests with mock drivers or misordered properties). Setting spring.jpa.database-platform=...PgJsonbDialect eliminates the guesswork by hard-wiring your class. For the broader “how Boot chooses a Dialect” discussion, the Boot docs are the place to verify behavior and property names. (Home)

2.6 Visual anchors you can refer back to

When onboarding teammates, point them to a simple architecture picture and a JSONB overview with diagrams. It reduces “black box” fear and shortens code review debates:

  • Hibernate high-level architecture image (simple block diagram). (tutorialspoint.com)
  • Formal Hibernate architecture text with diagram reference from Red Hat. (docs.redhat.com)
  • PostgreSQL JSON/JSONB docs with operator tables you’ll query against. (PostgreSQL)

3. Production notes: forward-looking choices you won’t regret

When the dust settles, teams that thrive with Postgres + Hibernate share a pattern: they keep type rules in the Dialect, semantics in annotations, and query ergonomics in function contributions. Stick to that, and new engineers can read your intent at a glance. If you later adopt extra Postgres types (citext, ltree), they drop into the same Dialect, and Boot keeps picking it up—no churn across dozens of entities.

As Hibernate evolves, APIs for function registration continue to modernize (Hibernate 6 changed the old registerFunction APIs; migration notes and 6.x how-tos are helpful references when you wire custom functions). Keep these links around for your next upgrade sprint. (Stack Overflow)

Have questions, edge cases, or want me to wire in jsonb_path_query for Criteria? Drop a comment below and tell me what your schema looks like—happy to sharpen this further.

Read more at : How to Create a Custom PostgreSQL Dialect (Hibernate 6) and Make Spring Boot Recognize It

More from this blog

T

tuanh.net

540 posts

Are you ready to elevate your Java, OOP, Spring, and DevOps skills? Look no further!