Custom Type Conversions: AttributeConverter and Enums

Introduction

JPA handles most Java types automatically — String, Integer, LocalDate, BigDecimal. But what about a Java enum? A List<String> stored as JSON? A custom Money type? JPA’s AttributeConverter and @Enumerated let you control exactly how any Java type maps to a database column.


Mapping Enums with @Enumerated

Java enums are common in domain models. JPA maps them with @Enumerated:

public enum OrderStatus {
    PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}

public enum ProductStatus {
    ACTIVE, INACTIVE, DISCONTINUED
}
@Column(length = 20, nullable = false)
@Enumerated(EnumType.STRING)
private OrderStatus status;

EnumType.STRING vs. EnumType.ORDINAL

EnumType.STRINGEnumType.ORDINAL
Stored as"PENDING", "SHIPPED" etc.0, 1, 2 etc.
Readable in DBYesNo
Safe on reorderYesNo — reordering breaks data
DefaultNoYes (JPA default)

Always use EnumType.STRING. Never use EnumType.ORDINAL — if you add a new enum value in the middle, every existing ordinal shifts and your data is corrupted.

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 20, nullable = false)
    @Enumerated(EnumType.STRING)
    private OrderStatus status = OrderStatus.PENDING;
}

AttributeConverter: Full Control Over Type Mapping

AttributeConverter<X, Y> converts between a Java type X and a database column type Y. Implement this interface for any type JPA can’t handle natively.

Example 1: Storing a List as a comma-separated column

@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {

    @Override
    public String convertToDatabaseColumn(List<String> list) {
        if (list == null || list.isEmpty()) return "";
        return String.join(",", list);
    }

    @Override
    public List<String> convertToEntityAttribute(String dbData) {
        if (dbData == null || dbData.isBlank()) return new ArrayList<>();
        return Arrays.asList(dbData.split(","));
    }
}

Apply it on the field:

@Entity
public class Product {

    @Convert(converter = StringListConverter.class)
    @Column(name = "search_keywords", length = 500)
    private List<String> searchKeywords;
}

Stored in DB: "electronics,gadgets,sale" Returned to Java: ["electronics", "gadgets", "sale"]

Example 2: Storing a custom Money value object

public class Money {
    private final BigDecimal amount;
    private final String currency;

    public Money(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    // getters, equals, hashCode
}
@Converter
public class MoneyConverter implements AttributeConverter<Money, String> {

    @Override
    public String convertToDatabaseColumn(Money money) {
        if (money == null) return null;
        return money.getAmount() + " " + money.getCurrency();
    }

    @Override
    public Money convertToEntityAttribute(String dbData) {
        if (dbData == null) return null;
        String[] parts = dbData.split(" ");
        return new Money(new BigDecimal(parts[0]), parts[1]);
    }
}
@Convert(converter = MoneyConverter.class)
@Column(name = "price", length = 30)
private Money price;

Example 3: Enum with a database code

Sometimes an enum must map to a specific database code, not its name:

public enum PaymentMethod {
    CREDIT_CARD("CC"),
    DEBIT_CARD("DC"),
    BANK_TRANSFER("BT"),
    PAYPAL("PP");

    private final String code;

    PaymentMethod(String code) { this.code = code; }
    public String getCode() { return code; }

    public static PaymentMethod fromCode(String code) {
        return Arrays.stream(values())
            .filter(pm -> pm.code.equals(code))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("Unknown code: " + code));
    }
}
@Converter
public class PaymentMethodConverter implements AttributeConverter<PaymentMethod, String> {

    @Override
    public String convertToDatabaseColumn(PaymentMethod method) {
        return method == null ? null : method.getCode();
    }

    @Override
    public PaymentMethod convertToEntityAttribute(String code) {
        return code == null ? null : PaymentMethod.fromCode(code);
    }
}
@Convert(converter = PaymentMethodConverter.class)
@Column(name = "payment_method", length = 2)
private PaymentMethod paymentMethod;

Stored in DB as "CC", "DC", etc. — compact and meaningful.


Auto-Apply Converters

Adding autoApply = true to @Converter makes it apply automatically to all fields of that type — no @Convert annotation needed on each field:

@Converter(autoApply = true)
public class PaymentMethodConverter implements AttributeConverter<PaymentMethod, String> {
    // same implementation
}

Now any PaymentMethod field in any entity is automatically converted without @Convert:

@Entity
public class Order {
    private PaymentMethod paymentMethod;  // auto-converted — no @Convert needed
}

Use autoApply = true when you have one canonical converter for a type and want it applied everywhere.


Date and Time Mappings

Spring Boot 3 with Hibernate 6 handles java.time types natively — no @Temporal annotation needed:

Java typeSQL typeNotes
LocalDateDATEDate only
LocalTimeTIMETime only
LocalDateTimeDATETIMENo timezone
ZonedDateTimeDATETIME + conversionTimezone-aware
InstantDATETIMEUTC instant
OffsetDateTimeDATETIMEWith offset
YearSMALLINTHibernate 6+
YearMonthVARCHAR / converterNeeds converter
@Column(name = "ordered_at", nullable = false)
private LocalDateTime orderedAt;

@Column(name = "delivery_date")
private LocalDate deliveryDate;

Hibernate 6 maps these automatically. You only need @Temporal for the old java.util.Date type — avoid that in new code.


JSON Column Mapping (MySQL JSON type)

MySQL 5.7+ supports a native JSON column type. With Hibernate 6, you can map a Java object directly to JSON:

// For Hibernate 6 with MySQL JSON columns, use @JdbcTypeCode
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "JSON")
private Map<String, Object> metadata;

Or map a custom POJO:

@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "JSON")
private ProductAttributes attributes;

This requires Jackson on the classpath (included by spring-boot-starter-web).


Boolean Mapping

MySQL’s TINYINT(1) is the standard for booleans:

@Column(name = "is_active", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 1")
private boolean active = true;

Hibernate maps Java boolean/Boolean to TINYINT(1) automatically for MySQL. No converter needed.


Applying Converters in Practice

The Product entity with enum and converter:

@Entity
@Table(name = "products")
@Getter @Setter @NoArgsConstructor
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 255)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    // Enum mapped as STRING
    @Column(length = 20, nullable = false)
    @Enumerated(EnumType.STRING)
    private ProductStatus status = ProductStatus.ACTIVE;

    // List<String> stored as comma-separated value
    @Convert(converter = StringListConverter.class)
    @Column(name = "tags", length = 1000)
    private List<String> tags = new ArrayList<>();

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
}

Key Takeaways

  • Always use @Enumerated(EnumType.STRING)EnumType.ORDINAL is dangerous and should be avoided
  • AttributeConverter<JavaType, DbType> gives you full control over type mapping for any Java type
  • @Converter(autoApply = true) applies a converter globally to all fields of that type
  • Hibernate 6 handles all java.time types natively — no @Temporal needed
  • For JSON columns in MySQL, use @JdbcTypeCode(SqlTypes.JSON) with a columnDefinition = "JSON"
  • Custom enum converters using a code field keep database values stable even when enum names change

What’s Next

Article 9 starts Part 3: Entity Relationships — beginning with @OneToOne, the simplest relationship type, covering both unidirectional and bidirectional mapping, foreign key placement, and cascade behaviour.