How to map a PostgreSQL Range column type with JPA and Hibernate

Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!


In this article, we are going to see how to map the PostgreSQL range column types with JPA and Hibernate.

Luckily, you don’t have to implement a custom Hibernate type for the PostgreSQL range column type since the hibernate-types project already provides support for it.

Range column types

PostgreSQL supports multiple range types:

  • int4range – Stores a range of integer values
  • int8range – Stores a range of bigint (e.g. java.util.Long) values
  • numrange – Stores a range of numeric (e.g. java.util.BigDecimal) values
  • daterange – Stores a range of timestamp (e.g. java.time.LocalDate) values
  • tsrange – Stores a range of timestamp (e.g. java.time.LocalDateTime) values
  • tstzrange – Stores a range of timestamp with timezone (e.g. java.time.ZonedDateTime) values

To represent ranges of values which can have open or closed lower and upper boundaries, we can use the Range class coming with the hibernate-types project.

Range entity

Range Hibernate Type

When mapping a Hibernate custom Type you have two options:

Using the former strategy, the PostgreSQLRangeType looks as follows:

public class PostgreSQLRangeType 
        extends ImmutableType<Range> {

    public PostgreSQLRangeType() {

    public int[] sqlTypes() {
        return new int[]{Types.OTHER};

    protected Range get(
            ResultSet rs, 
            String[] names, 
            SharedSessionContractImplementor session, 
            Object owner) 
        throws SQLException {
        Object pgObject = rs.getObject(names[0]);
        String type = ReflectionUtils.invokeGetter(
        String value = ReflectionUtils.invokeGetter(

        switch (type) {
            case "int4range":
                return Range.integerRange(value);
            case "int8range":
                return Range.longRange(value);
            case "numrange":
                return Range.bigDecimalRange(value);
            case "tsrange":
                return Range.localDateTimeRange(value);
            case "tstzrange":
                return Range.zonedDateTimeRange(value);
            case "daterange":
                return Range.localDateRange(value);
                throw new IllegalStateException(
                    "The range type [" + type + "] is not supported!"

    protected void set(
            PreparedStatement st, 
            Range range, 
            int index, 
            SharedSessionContractImplementor session) 
        throws SQLException {

        if (range == null) {
            st.setNull(index, Types.OTHER);
        } else {
            Object holder = ReflectionUtils.newInstance(


            st.setObject(index, holder);

    private static String determineRangeType(Range<?> range) {
        Class<?> clazz = range.getClazz();

        if (clazz.equals(Integer.class)) {
            return "int4range";
        } else if (clazz.equals(Long.class)) {
            return "int8range";
        } else if (clazz.equals(BigDecimal.class)) {
            return "numrange";
        } else if (clazz.equals(LocalDateTime.class)) {
            return "tsrange";
        } else if (clazz.equals(ZonedDateTime.class)) {
            return "tstzrange";
        } else if (clazz.equals(LocalDate.class)) {
            return "daterange";

        throw new IllegalStateException(
            "The class [" + clazz.getName() + "] is not supported!"

If you want to implement a custom Hibernate type by implementing the UserType interface, it’s much easier if you just extend the ImmutableType offered by the hibernate-types project.

For more details, check out this article.

Maven dependency

As already mentioned, you don’t need to create the Range or the PostgreSQLRangeType classes. You can get them via the hibernate-types Maven dependency:


If you’re using an older version of Hibernate, go to the hibernate-types GitHub repository and find the matching hibernate-types dependency for your current Hibernate version.

Domain Model

Let’s assume we are developing a bookstore application, and the Book entities look as follows:

@Entity(name = "Book")
@Table(name = "book")
    typeClass = PostgreSQLRangeType.class, 
    defaultForType = Range.class
public class Book {

    private Long id;

    private String isbn;

    private String title;

        name = "price_cent_range", 
        columnDefinition = "numrange"
    private Range<BigDecimal> priceRange;

        name = "discount_date_range", 
        columnDefinition = "daterange"
    private Range<LocalDate> discountDateRange;

    //Getters and setters omitted for brevity

Notice the use of the @TypeDef annotation which tells Hibernate to use the PostgreSQLRangeType Hibernate Type for handling the Range entity properties.

The isbn property is marked with the @NaturalId Hibernate-specific annotation which allows us to retrieve the Book entity via its natural identifier. For more details about using natural identifiers, check out this article.

used for the which is very useful for mapping business keys.

Testing time

Now, when persisting the following two Book entities:

Book book = new Book();
book.setTitle("High-Performance Java Persistence");
        LocalDate.of(2019, 11, 29),
        LocalDate.of(2019, 12, 3)


Hibernate generates the following SQL INSERT statements:

    'High-Performance Java Persistence',

When fetching the previously persisted Book entity, we can see that the range properties are properly retrieved from the underlying database columns:

Book book = entityManager



    LocalDate.of(2019, 11, 29), 

    LocalDate.of(2019, 12, 3), 

What’s nice about using the range column types is that we can use range-specific operators like the @> one, which verifies if the provided value is contained in the range interval:

List<Book> discountedBooks = entityManager
    "SELECT * " +
    "FROM book b " +
    "WHERE " +
    "   b.discount_date_range @> CAST(:today AS date) = true ", Book.class)
    LocalDate.of(2019, 12, 1)

        book -> book.getTitle().equals("High-Performance Java Persistence")

Cool, right?

I'm running an online workshopk on the 14th of May about The Best Way to Fetch Data with Java Persistence and Hibernate.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.


Mapping non-standard database column types is quite straightforward with Hibernate. However, with the help of the hibernate-types project, you don’t even have to write all these types.

Just add the Maven dependency to your project pom.xml configuration file, and provide the @TypeDef annotation to the entity in question, and start mapping JSON, ARRAY, PostgreSQL Enum or Inet addresses.

Transactions and Concurrency Control eBook

6 Comments on “How to map a PostgreSQL Range column type with JPA and Hibernate

  1. Hi Vlad,
    Thank you for your article.
    I’m trying to cast Range to daterange in the native query : CAST(:range AS daterange) and seem like it’s not working. It throws an error: cannot cast type bytea to daterange.
    Do you have any suggestion to solve this problem? Thank you very much

    • Send me a Pull Request with a replicating test case in my Hibernate-Types repository so I can take a look.

  2. Can we use it inside a JPA Specification ?
    Or how can we create predicate with this ?

    • That’s a Spring thingy. You have to ask the Spring team how it can be done.

  3. How to use a special mark @ in my JpaRepository in Spring Boot?

    @Repository public interface myInterface extends JpaRepository { @Query(“Select field from myTable where field2@>3”) String findRequiredActionByNote(@Param(“note”) Integer note); }

    doesn’t work: unexpected char: ‘@’

    • You need to use a native query. You Query annotation is parsed as JPQL. Set the naticeQuery attribute to true.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.