How to Map PostgreSQL Array Types (text[]) with Spring Boot + JPA
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 Array → String[] conversion. This was easier than JPA.
Summary
| Approach | Difficulty | Notes |
|---|---|---|
String[] as-is | ✗ | Mapping error |
columnDefinition = "text[]" only | △ | Possible runtime errors |
Custom UserType implementation | ⚠️ Hard | Implement all type conversion yourself |
| hypersistence-utils | ✅ Easy | Add library to resolve |
| MyBatis | ✅ Easy | Automatic 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.