Custom UserTypes in Hibernate allow you to map complex Java types to database columns that aren't supported by default. This is particularly useful for JSON objects, custom data structures, enums with complex behavior, and other non-standard type mappings.
What are UserTypes?
UserTypes are Hibernate's extension point for custom type mappings. They define how to:
- Transform Java objects to JDBC types for storage
- Transform JDBC types back to Java objects
- Handle null values and equality checks
Key Interfaces:
org.hibernate.usertype.UserType- Basic custom typeorg.hibernate.usertype.CompositeUserType- For composite typesorg.hibernate.usertype.EnhancedUserType- For additional capabilities
Basic UserType Implementation
Example 1: JSON UserType for Storing Objects as JSON
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.UserType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.Serializable;
import java.sql.*;
import java.util.Objects;
/**
* Custom UserType for storing any Java object as JSON in the database
*/
public class JsonUserType implements UserType {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final Class<?> returnedClass;
public JsonUserType(Class<?> returnedClass) {
this.returnedClass = returnedClass;
}
@Override
public int[] sqlTypes() {
return new int[]{Types.JAVA_OBJECT}; // or Types.VARCHAR for text-based JSON
}
@Override
public Class<?> returnedClass() {
return returnedClass;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return Objects.equals(x, y);
}
@Override
public int hashCode(Object x) throws HibernateException {
return Objects.hashCode(x);
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
String jsonValue = rs.getString(names[0]);
if (jsonValue == null || jsonValue.isEmpty()) {
return null;
}
try {
return OBJECT_MAPPER.readValue(jsonValue, returnedClass);
} catch (Exception e) {
throw new HibernateException("Error parsing JSON from database", e);
}
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, Types.VARCHAR);
} else {
try {
String json = OBJECT_MAPPER.writeValueAsString(value);
st.setString(index, json);
} catch (Exception e) {
throw new HibernateException("Error converting object to JSON", e);
}
}
}
@Override
public Object deepCopy(Object value) throws HibernateException {
if (value == null) {
return null;
}
try {
// Serialize and deserialize to create a deep copy
String json = OBJECT_MAPPER.writeValueAsString(value);
return OBJECT_MAPPER.readValue(json, returnedClass);
} catch (Exception e) {
throw new HibernateException("Error creating deep copy", e);
}
}
@Override
public boolean isMutable() {
return true;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) deepCopy(value);
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return deepCopy(cached);
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return deepCopy(original);
}
}
Example 2: Using the JSON UserType
import org.hibernate.annotations.Type;
import javax.persistence.*;
import java.util.Map;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
// Custom JSON type for storing product attributes
@Column(name = "attributes", columnDefinition = "jsonb") // PostgreSQL jsonb type
@Type(type = "com.example.types.JsonUserType",
parameters = @org.hibernate.annotations.Parameter(
name = "returnedClass",
value = "java.util.Map"
))
private Map<String, Object> attributes;
// Custom JSON type for storing metadata
@Column(name = "metadata", columnDefinition = "jsonb")
@Type(type = "com.example.types.JsonUserType",
parameters = @org.hibernate.annotations.Parameter(
name = "returnedClass",
value = "com.example.types.ProductMetadata"
))
private ProductMetadata metadata;
// Constructors, getters, and setters
public Product() {}
public Product(String name, Double price, Map<String, Object> attributes) {
this.name = name;
this.price = price;
this.attributes = attributes;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public Map<String, Object> getAttributes() { return attributes; }
public void setAttributes(Map<String, Object> attributes) { this.attributes = attributes; }
public ProductMetadata getMetadata() { return metadata; }
public void setMetadata(ProductMetadata metadata) { this.metadata = metadata; }
}
// Custom metadata class
public class ProductMetadata {
private String createdBy;
private String category;
private Map<String, String> tags;
private AuditInfo auditInfo;
// Constructors, getters, and setters
public ProductMetadata() {}
public ProductMetadata(String createdBy, String category) {
this.createdBy = createdBy;
this.category = category;
}
// Getters and setters
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public Map<String, String> getTags() { return tags; }
public void setTags(Map<String, String> tags) { this.tags = tags; }
public AuditInfo getAuditInfo() { return auditInfo; }
public void setAuditInfo(AuditInfo auditInfo) { this.auditInfo = auditInfo; }
}
public class AuditInfo {
private String createdBy;
private java.time.LocalDateTime createdAt;
private String modifiedBy;
private java.time.LocalDateTime modifiedAt;
// Constructors, getters, and setters
}
Advanced UserType Implementations
Example 3: Composite UserType for Complex Objects
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.CompositeUserType;
import java.io.Serializable;
import java.sql.*;
/**
* Composite UserType for storing Money objects (amount and currency)
*/
public class MoneyUserType implements CompositeUserType {
@Override
public String[] getPropertyNames() {
return new String[]{"amount", "currency"};
}
@Override
public Type[] getPropertyTypes() {
return new Type[]{
org.hibernate.type.StandardBasicTypes.BIG_DECIMAL,
org.hibernate.type.StandardBasicTypes.STRING
};
}
@Override
public Object getPropertyValue(Object component, int property) throws HibernateException {
Money money = (Money) component;
return switch (property) {
case 0 -> money.getAmount();
case 1 -> money.getCurrency();
default -> throw new HibernateException("Invalid property index: " + property);
};
}
@Override
public void setPropertyValue(Object component, int property, Object value) throws HibernateException {
Money money = (Money) component;
switch (property) {
case 0 -> money.setAmount((java.math.BigDecimal) value);
case 1 -> money.setCurrency((String) value);
default -> throw new HibernateException("Invalid property index: " + property);
}
}
@Override
public Class returnedClass() {
return Money.class;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
if (x == y) return true;
if (x == null || y == null) return false;
return x.equals(y);
}
@Override
public int hashCode(Object x) throws HibernateException {
return x.hashCode();
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
java.math.BigDecimal amount = rs.getBigDecimal(names[0]);
if (rs.wasNull()) {
return null;
}
String currency = rs.getString(names[1]);
return new Money(amount, currency);
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, Types.DECIMAL);
st.setNull(index + 1, Types.VARCHAR);
} else {
Money money = (Money) value;
st.setBigDecimal(index, money.getAmount());
st.setString(index + 1, money.getCurrency());
}
}
@Override
public Object deepCopy(Object value) throws HibernateException {
if (value == null) {
return null;
}
Money original = (Money) value;
return new Money(original.getAmount(), original.getCurrency());
}
@Override
public boolean isMutable() {
return true;
}
@Override
public Serializable disassemble(Object value, SharedSessionContractImplementor session) throws HibernateException {
return (Serializable) deepCopy(value);
}
@Override
public Object assemble(Serializable cached, SharedSessionContractImplementor session, Object owner)
throws HibernateException {
return deepCopy(cached);
}
@Override
public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner)
throws HibernateException {
return deepCopy(original);
}
}
// Money value object
public class Money {
private java.math.BigDecimal amount;
private String currency; // ISO currency code like "USD", "EUR"
public Money() {}
public Money(java.math.BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// Business methods
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// Getters and setters
public java.math.BigDecimal getAmount() { return amount; }
public void setAmount(java.math.BigDecimal amount) { this.amount = amount; }
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return Objects.equals(amount, money.amount) &&
Objects.equals(currency, money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
@Override
public String toString() {
return amount + " " + currency;
}
}
Example 4: Using Composite UserType
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
@Type(type = "com.example.types.MoneyUserType")
@Columns(columns = {
@Column(name = "total_amount"),
@Column(name = "currency_code", length = 3)
})
private Money totalAmount;
@Type(type = "com.example.types.MoneyUserType")
@Columns(columns = {
@Column(name = "tax_amount"),
@Column(name = "tax_currency", length = 3)
})
private Money taxAmount;
// Constructors, getters, and setters
public Order() {}
public Order(String orderNumber, Money totalAmount, Money taxAmount) {
this.orderNumber = orderNumber;
this.totalAmount = totalAmount;
this.taxAmount = taxAmount;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getOrderNumber() { return orderNumber; }
public void setOrderNumber(String orderNumber) { this.orderNumber = orderNumber; }
public Money getTotalAmount() { return totalAmount; }
public void setTotalAmount(Money totalAmount) { this.totalAmount = totalAmount; }
public Money getTaxAmount() { return taxAmount; }
public void setTaxAmount(Money taxAmount) { this.taxAmount = taxAmount; }
}
Enum UserType with Custom Mapping
Example 5: Custom Enum UserType
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.UserType;
import java.sql.*;
import java.util.Objects;
/**
* UserType for storing enums with custom database representations
*/
public class EnumUserType<E extends Enum<E>> implements UserType {
private final Class<E> clazz;
private final String databaseType;
public EnumUserType(Class<E> clazz, String databaseType) {
this.clazz = clazz;
this.databaseType = databaseType;
}
@Override
public int[] sqlTypes() {
return new int[]{Types.VARCHAR}; // Store as string
}
@Override
public Class<E> returnedClass() {
return clazz;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return Objects.equals(x, y);
}
@Override
public int hashCode(Object x) throws HibernateException {
return Objects.hashCode(x);
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
String value = rs.getString(names[0]);
if (value == null) {
return null;
}
try {
return Enum.valueOf(clazz, value.toUpperCase());
} catch (IllegalArgumentException e) {
throw new HibernateException("Invalid value " + value + " for enum " + clazz.getName(), e);
}
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, Types.VARCHAR);
} else {
st.setString(index, ((Enum<?>) value).name().toLowerCase());
}
}
@Override
public Object deepCopy(Object value) throws HibernateException {
return value; // Enums are immutable
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return cached;
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
}
// Custom enum with database representation
public enum OrderStatus {
PENDING("pending"),
CONFIRMED("confirmed"),
SHIPPED("shipped"),
DELIVERED("delivered"),
CANCELLED("cancelled");
private final String dbValue;
OrderStatus(String dbValue) {
this.dbValue = dbValue;
}
public String getDbValue() {
return dbValue;
}
public static OrderStatus fromDbValue(String dbValue) {
for (OrderStatus status : values()) {
if (status.dbValue.equals(dbValue)) {
return status;
}
}
throw new IllegalArgumentException("Unknown database value: " + dbValue);
}
}
Example 6: Using Enum UserType
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
@Column(name = "status")
@Type(type = "com.example.types.EnumUserType",
parameters = {
@org.hibernate.annotations.Parameter(name = "clazz",
value = "com.example.types.OrderStatus"),
@org.hibernate.annotations.Parameter(name = "databaseType",
value = "varchar")
})
private OrderStatus status;
// Constructors, getters, and setters
public Order() {}
public Order(String orderNumber, OrderStatus status) {
this.orderNumber = orderNumber;
this.status = status;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getOrderNumber() { return orderNumber; }
public void setOrderNumber(String orderNumber) { this.orderNumber = orderNumber; }
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus status) { this.status = status; }
}
Array and Collection UserTypes
Example 7: String Array UserType
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.UserType;
import java.sql.*;
import java.util.Arrays;
import java.util.Objects;
/**
* UserType for storing string arrays in PostgreSQL
*/
public class StringArrayUserType implements UserType {
@Override
public int[] sqlTypes() {
return new int[]{Types.ARRAY}; // PostgreSQL array type
}
@Override
public Class returnedClass() {
return String[].class;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
if (x == y) return true;
if (x == null || y == null) return false;
return Arrays.equals((String[]) x, (String[]) y);
}
@Override
public int hashCode(Object x) throws HibernateException {
return Arrays.hashCode((String[]) x);
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
Array array = rs.getArray(names[0]);
if (array == null) {
return null;
}
return array.getArray();
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, Types.ARRAY);
} else {
String[] array = (String[]) value;
Array sqlArray = session.connection().createArrayOf("varchar", array);
st.setArray(index, sqlArray);
}
}
@Override
public Object deepCopy(Object value) throws HibernateException {
if (value == null) {
return null;
}
String[] original = (String[]) value;
return Arrays.copyOf(original, original.length);
}
@Override
public boolean isMutable() {
return true;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) deepCopy(value);
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return deepCopy(cached);
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return deepCopy(original);
}
}
Example 8: Using Array UserType
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(name = "tags")
@Type(type = "com.example.types.StringArrayUserType")
private String[] tags;
@Column(name = "image_urls")
@Type(type = "com.example.types.StringArrayUserType")
private String[] imageUrls;
// Constructors, getters, and setters
public Product() {}
public Product(String name, String[] tags, String[] imageUrls) {
this.name = name;
this.tags = tags;
this.imageUrls = imageUrls;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String[] getTags() { return tags; }
public void setTags(String[] tags) { this.tags = tags; }
public String[] getImageUrls() { return imageUrls; }
public void setImageUrls(String[] imageUrls) { this.imageUrls = imageUrls; }
}
Configuration and Registration
Example 9: TypeDef Registration
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
// Register custom types at package or class level
@TypeDefs({
@TypeDef(
name = "json",
typeClass = JsonUserType.class,
parameters = @org.hibernate.annotations.Parameter(
name = "returnedClass",
value = "java.util.Map"
)
),
@TypeDef(
name = "money",
typeClass = MoneyUserType.class
),
@TypeDef(
name = "string-array",
typeClass = StringArrayUserType.class
)
})
@Entity
@Table(name = "products")
public class Product {
// Now you can use simplified @Type annotations
@Type(type = "json")
private Map<String, Object> attributes;
@Type(type = "money")
private Money price;
@Type(type = "string-array")
private String[] tags;
}
Example 10: Spring Boot Configuration
@Configuration
public class HibernateConfig {
@Bean
public LocalSessionFactoryBean sessionFactory(DataSource dataSource) {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setPackagesToScan("com.example.entity");
Properties hibernateProperties = new Properties();
hibernateProperties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
hibernateProperties.put("hibernate.show_sql", "true");
hibernateProperties.put("hibernate.format_sql", "true");
sessionFactory.setHibernateProperties(hibernateProperties);
return sessionFactory;
}
}
Testing Custom UserTypes
Example 11: Unit Testing UserTypes
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import java.sql.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class JsonUserTypeTest {
private JsonUserType userType;
private ResultSet resultSet;
private PreparedStatement preparedStatement;
@BeforeEach
void setUp() {
userType = new JsonUserType(Map.class);
resultSet = mock(ResultSet.class);
preparedStatement = mock(PreparedStatement.class);
}
@Test
void testNullSafeGet() throws Exception {
String json = "{\"key\": \"value\", \"number\": 123}";
when(resultSet.getString("attributes")).thenReturn(json);
Object result = userType.nullSafeGet(resultSet, new String[]{"attributes"}, null, null);
assertNotNull(result);
assertTrue(result instanceof Map);
Map<?, ?> map = (Map<?, ?>) result;
assertEquals("value", map.get("key"));
assertEquals(123, map.get("number"));
}
@Test
void testNullSafeSet() throws Exception {
Map<String, Object> data = Map.of("name", "Test", "active", true);
userType.nullSafeSet(preparedStatement, data, 0, null);
verify(preparedStatement).setString(0, "{\"name\":\"Test\",\"active\":true}");
}
@Test
void testDeepCopy() {
Map<String, Object> original = Map.of("data", "test");
Object copy = userType.deepCopy(original);
assertNotSame(original, copy);
assertEquals(original, copy);
}
}
Best Practices and Considerations
Performance Tips:
- Caching: Consider caching in immutable UserTypes
- Connection Handling: Use session-provided connections
- Lazy Loading: Implement proper lazy loading for large objects
- Batch Operations: Ensure UserTypes work with batch operations
Error Handling:
- Validation: Validate data before conversion
- Exception Handling: Provide meaningful error messages
- Null Safety: Always handle null values properly
- Type Safety: Validate types during conversion
Database Compatibility:
- Dialect Support: Test with different database dialects
- Type Mapping: Ensure proper SQL type mappings
- Vendor Features: Leverage database-specific features when available
Conclusion
Custom UserTypes provide powerful capabilities for:
Common Use Cases:
- JSON/XML object storage
- Complex value objects (Money, Address, etc.)
- Custom enum mappings
- Array and collection storage
- Encryption/decryption of sensitive data
- Compression of large text/data
Key Benefits:
- Type Safety: Compile-time type checking
- Encapsulation: Business logic stays in domain objects
- Performance: Optimized database interactions
- Maintainability: Clean separation of concerns
- Reusability: Share UserTypes across entities
When to Use Custom UserTypes:
- When default Hibernate type mappings are insufficient
- For complex object-to-database mappings
- When you need custom serialization/deserialization
- For database-specific optimizations
- When working with legacy database schemas
Custom UserTypes are a powerful Hibernate feature that bridges the gap between object-oriented domain models and relational database schemas, enabling clean, maintainable, and performant data access layers.