Tech Blog

How to Map PostgreSQL Array Types (text[]) with Spring Boot + JPA

by y104
PostgreSQL Spring Boot JPA Java

Introduction

PostgreSQL has array types like text[] and integer[].
When I tried to use them for a requirement to “save film tags as arrays,” I ran into quite a few problems with the Spring Boot + JPA combination.

This article summarizes those pain points and solutions.


What I Wanted to Do

Auto-generate mood tags for films using LLM and save them to the taste_tags column in the film table.

ALTER TABLE film ADD COLUMN taste_tags text[];

Image of stored data:

UPDATE film SET taste_tags = ARRAY['Action', 'Thrilling', 'Family-friendly'] WHERE film_id = 1;

I wanted to receive this in a Java entity.


First Approach (Failed)

First I straightforwardly tried mapping with String[].

@Entity
@Table(name = "film")
public class Film {
    // ... omitted ...
    
    @Column(name = "taste_tags")
    private String[] tasteTags;  // Thought this would work
}

Result: Error on startup

org.hibernate.MappingException: Could not determine type for: [Ljava.lang.String;

Hibernate does not automatically map Java’s String[] to PostgreSQL’s text[].


Solution 1: Explicitly Specify columnDefinition

@Column(name = "taste_tags", columnDefinition = "text[]")
private String[] tasteTags;

This removes the startup error, but a different error may appear at runtime.

ERROR: column "taste_tags" is of type text[] but expression is of type character varying

When Hibernate generates SQL, it sometimes doesn’t handle text[] correctly.


Solution 2: Implement a Custom UserType (Hibernate 6.x)

In Hibernate 6 and later, you can implement the UserType interface to create custom types.

public class StringArrayType implements UserType<String[]> {
    
    @Override
    public int getSqlType() {
        return Types.ARRAY;
    }
    
    @Override
    public Class<String[]> returnedClass() {
        return String[].class;
    }
    
    @Override
    public String[] nullSafeGet(ResultSet rs, int position, 
            SharedSessionContractImplementor session, Object owner) 
            throws SQLException {
        Array array = rs.getArray(position);
        if (array == null) return null;
        return (String[]) array.getArray();
    }
    
    @Override
    public void nullSafeSet(PreparedStatement st, String[] value, int index,
            SharedSessionContractImplementor session) 
            throws SQLException {
        if (value == null) {
            st.setNull(index, Types.ARRAY);
        } else {
            Array array = st.getConnection().createArrayOf("text", value);
            st.setArray(index, array);
        }
    }
    
    // equals, hashCode, deepCopy, isMutable, disassemble, assemble also need implementation
    @Override
    public boolean equals(String[] x, String[] y) {
        return Arrays.equals(x, y);
    }
    
    @Override
    public int hashCode(String[] x) {
        return Arrays.hashCode(x);
    }
    
    @Override
    public String[] deepCopy(String[] value) {
        return value == null ? null : Arrays.copyOf(value, value.length);
    }
    
    @Override
    public boolean isMutable() { return true; }
    
    @Override
    public Serializable disassemble(String[] value) {
        return deepCopy(value);
    }
    
    @Override
    public String[] assemble(Serializable cached, Object owner) {
        return deepCopy((String[]) cached);
    }
}

Using it in an entity:

@Column(name = "taste_tags", columnDefinition = "text[]")
@Type(StringArrayType.class)
private String[] tasteTags;

Solution 3: Use the Hibernate Types Library (Simplest)

Using the library vladmihalcea/hibernate-types lets you skip implementing custom types.

<!-- pom.xml -->
<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-63</artifactId>
    <version>3.7.0</version>
</dependency>
import io.hypersistence.utils.hibernate.type.array.StringArrayType;

@Entity
@TypeDef(name = "string-array", typeClass = StringArrayType.class)
public class Film {
    
    @Type(type = "string-array")
    @Column(name = "taste_tags", columnDefinition = "text[]")
    private String[] tasteTags;
}

Since @TypeDef is deprecated in Hibernate 6.x (Spring Boot 3.x), the following is the current recommended way:

@Column(name = "taste_tags", columnDefinition = "text[]")
@Type(io.hypersistence.utils.hibernate.type.array.StringArrayType.class)
private String[] tasteTags;

Alternative Approach with MyBatis

In this project, the batch processing side (admin screen) uses JPA,
but the customer-facing API is implemented with MyBatis, which makes handling String[] simpler.

// Receiving in MyBatis ResultMap
public class FilmDetail {
    private String[] tasteTags;
    // Can receive JDBC Array as String array
}
<!-- Mapper XML -->
<select id="findById" resultType="FilmDetail">
    SELECT taste_tags FROM film WHERE film_id = #{filmId}
</select>

MyBatis automatically handles the ArrayString[] conversion. This was easier than JPA.


Summary

ApproachDifficultyNotes
String[] as-isMapping error
columnDefinition = "text[]" onlyPossible runtime errors
Custom UserType implementation⚠️ HardImplement all type conversion yourself
hypersistence-utils✅ EasyAdd library to resolve
MyBatis✅ EasyAutomatic conversion

If you want to use PostgreSQL array types, using hypersistence-utils is the best approach at this point.
If you’re using MyBatis, it works simply without additional libraries.


Article Map for This Series

Building an End-User DVD Rental App — Vue 3 + Spring Boot Paired with the Admin App, with Article Map

Feel free to send a message

Please send a message if you have any technical questions, feedback, or inquiries.