エンティティのステータス… Here’s the English translation:
When you represent an entity’s status or category with an enum, your code suddenly becomes much more readable. But once it comes time to persist the value to the database, you run into problems like: “It’s stored as a number and I can’t tell what it means,” “I reordered the enum and the meaning of existing data got shifted,” or “I want to map it to legacy DB code values like 'A' / 'B'.”
In this article, we’ll organize the two ways to persist enums with JPA — @Enumerated and AttributeConverter — and explain which one you should choose. Representing enums in JSON responses and input validation are a separate layer from persistence, so we won’t cover them here. Let’s focus on saving to the database.
Two approaches to saving enums in the database
There are broadly two ways to do this.
@Enumerated… The JPA standard. Stores the value directly using either the enum name (STRING) or the declaration-order number (ORDINAL).AttributeConverter… You write your own conversion logic. It lets you map an enum bidirectionally to a custom code value like'A'.
Roughly speaking, if you’re designing a new schema yourself, @Enumerated(EnumType.STRING) is enough. If you need to match existing DB code values or short codes, that’s where AttributeConverter comes in. Let’s start with the standard @Enumerated.
The basics of @Enumerated: how STRING and ORDINAL change the stored value
Let’s use an entity that holds order status as an enum as our example.
public enum OrderStatus {
PENDING, ACTIVE, SHIPPED, CANCELLED
}
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private OrderStatus status;
// getter / setter
}
@Enumerated has two modes. EnumType.STRING stores the enum name directly as a string, while EnumType.ORDINAL stores the declaration-order index (a zero-based number). Even for the same ACTIVE, the stored value is completely different.
-- With EnumType.STRING
| id | status |
|----|-----------|
| 1 | ACTIVE |
| 2 | SHIPPED |
-- With EnumType.ORDINAL
| id | status |
|----|--------|
| 1 | 1 |
| 2 | 2 |
The most important thing to note here is that the default when you omit @Enumerated is ORDINAL. If you write an enum field without any annotation, it will unintentionally be stored as a number. This leads directly to the next pitfall.
The ORDINAL pitfall: reordering or inserting in the middle corrupts your data
ORDINAL depends on the enum’s “declaration order.” In other words, if you later modify the enum definition, the meaning of the numbers already stored in the DB silently shifts.
For example, suppose you later insert PAID into the middle of the earlier enum.
public enum OrderStatus {
PENDING, PAID, ACTIVE, SHIPPED, CANCELLED
// The moment PAID is inserted, all indexes from ACTIVE onward shift
}
Just by changing the code, ACTIVE shifted from 1 to 2, and SHIPPED shifted from 2 to 3. But the DB still holds the old numbers. A record previously saved as ACTIVE (=1) will now be interpreted as PAID the next time it’s read. Unless you write a migration, a single-line code change destroys your existing data wholesale.
What’s more, since only the number remains in the DB, even if you look directly at the table in SQL, you can’t tell what 1 refers to. Operational readability is terrible too. As a rule, avoid ORDINAL.
Why STRING should be the default, and the caveats that remain
For this reason, in practice you make EnumType.STRING the default. Since it’s stored by enum name, it’s resistant to reordering and mid-list insertion, and its meaning is clear at a glance in SQL.
That said, STRING isn’t unconditionally safe either. There are two points to keep in mind.
- Reserve enough column length. If a long constant name doesn’t fit, you’ll get an error on save. Give yourself some margin for the maximum length of your enum names, like
@Column(length = 20). - Be careful when refactoring enum constant names. If you rename
ACTIVEtoIN_PROGRESS, it will no longer match the string'ACTIVE'remaining in the DB. Since the constant name becomes the DB value directly, you need to treat renames together with data migration.
If you want to fundamentally decouple this “renaming causes inconsistency with DB values” problem, the following AttributeConverter is effective.
Saving custom code values with AttributeConverter
With legacy DBs, status is often stored as a single-character code like 'A' (Active) or 'C' (Cancelled). When you want to map an enum to such custom code values, you implement an AttributeConverter.
First, give the enum itself a code value.
public enum OrderStatus {
ACTIVE("A"),
SHIPPED("S"),
CANCELLED("C");
private final String code;
OrderStatus(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
Next, write a Converter that converts bidirectionally between the enum and the DB value. convertToDatabaseColumn handles saving (enum → DB value), and convertToEntityAttribute handles reading (DB value → enum).
@Converter
public class OrderStatusConverter
implements AttributeConverter<OrderStatus, String> {
@Override
public String convertToDatabaseColumn(OrderStatus status) {
return status == null ? null : status.getCode();
}
@Override
public OrderStatus convertToEntityAttribute(String code) {
if (code == null) {
return null;
}
return Arrays.stream(OrderStatus.values())
.filter(s -> s.getCode().equals(code))
.findFirst()
.orElseThrow(() ->
new IllegalArgumentException("Unknown code value: " + code));
}
}
There are two ways to apply it: either attach it explicitly per field, or apply it automatically to all fields of that enum type.
// Apply per field
@Convert(converter = OrderStatusConverter.class)
private OrderStatus status;
// Apply automatically to every field where the enum type appears
@Converter(autoApply = true)
public class OrderStatusConverter
implements AttributeConverter<OrderStatus, String> { /* ... */ }
Setting autoApply = true makes it take effect automatically on every column that uses OrderStatus, so you’ll never forget to add @Convert. On the other hand, if you want to use a different storage scheme for the same enum depending on the location, per-field @Convert is more flexible. Note that the target type isn’t limited to String. If you make it AttributeConverter<OrderStatus, Integer>, you can apply it directly to numeric codes like 1 / 2 as well.
Handling unknown codes and null values
Something that’s surprisingly easy to forget in a Converter is handling unexpected values and null.
First, let null pass through as the basic rule. As in the code example above, if the input is null, return null without converting. If you throw an exception here, you’ll get accidents like a read failing on a NULLable column.
Next, the case where a code not in the definition (for example 'X') has gotten into the DB. Here, the approach splits into two.
- Throw an exception … When you want to detect invalid data early. Fail with
orElseThrowas in the example above, and if you log it, investigating the source of the contamination becomes easier. - Return null … When you can tolerate some missing data and don’t want to stop the app. However, since it silently becomes null, it assumes null checks in downstream processing.
There’s no single correct answer; you decide based on how much you trust your data quality. Personally, I think it’s safer to throw an exception at conversion time so you notice, rather than letting a null of unknown origin flow through to later stages.
Criteria for choosing between @Enumerated and AttributeConverter
Finally, let’s organize the guidelines for choosing in a table.
| Situation | Recommendation |
|---|---|
| Designing a new schema DDL-first | @Enumerated(EnumType.STRING) |
| Matching existing code values in a legacy DB | AttributeConverter |
Storing with short codes ('A') or numeric codes | AttributeConverter |
| Decoupling enum name changes from DB values | AttributeConverter |
| Keeping things as simple as possible | @Enumerated(EnumType.STRING) |
The key point is whether you can decide the DB-side value yourself. If you’re building from scratch, just go with STRING. If you’re matching existing values or short codes, or if you want to evolve enum names and DB values independently, choose AttributeConverter — that decision rarely gives you trouble.
Summary
For persisting enums, you’ll be fine remembering: the default is @Enumerated(EnumType.STRING), and use AttributeConverter when you need custom code values. Avoid ORDINAL as a rule, since reordering corrupts your data.
To further refine your entity design, check out Entity Relationship Mapping, which covers how to set up relationships, and JPA Query Methods for search processing. Representing enums in JSON responses, which we placed out of scope this time, is covered in the Jackson serialization article.