java-recipes

ホーム Enum(列挙型) › En-05

En-05: Enum のシリアライズ・JSON 変換

Enum を JSON や DB に保存・復元する際の実践的なパターンを解説します。ordinal() 保存の危険性と、 固定コード値による安全な永続化方法を理解しましょう。

いつ使うか

  • Enum の値を DB に保存・読み込みするとき
  • Enum を JSON に変換して API で送受信するとき
  • Java の標準シリアライズ(ObjectOutputStream)で Enum を扱うとき

永続化方式の比較

方式安全性備考
ordinal()0, 1, 2危険定義順変更で壊れる
name()PROCESSING注意定数名変更で壊れる
固定コード値PROC安全推奨。順序変更・リネームに強い

サンプルコード

EnumSerializeSample.java
import java.io.Serializable;

public class EnumSerializeSample {

    // ❌ ordinal による保存(脆弱な方式)
    // Enum の順番が変わると過去データが壊れる
    enum BadStatus {
        ACTIVE,    // ordinal=0
        INACTIVE,  // ordinal=1
        DELETED    // ordinal=2
    }

    // ✅ 推奨: 明示的なコード値で永続化
    enum OrderStatus {
        PENDING("PEND"),
        PROCESSING("PROC"),
        COMPLETED("COMP"),
        CANCELLED("CANC");

        private final String dbCode; // DB・JSON保存用の固定コード

        OrderStatus(String dbCode) {
            this.dbCode = dbCode;
        }

        public String getDbCode() {
            return dbCode;
        }

        // DB・JSON のコード値から Enum に変換
        public static OrderStatus fromDbCode(String code) {
            for (OrderStatus s : values()) {
                if (s.dbCode.equals(code)) {
                    return s;
                }
            }
            throw new IllegalArgumentException("不明なコード: " + code);
        }
    }

    // Java 標準シリアライズ(Enum は自動的に Serializable)
    // Enum はシングルトンが保証される(デシリアライズ後も同一インスタンス)
    static class Order implements Serializable {
        private static final long serialVersionUID = 1L;
        final String orderId;
        final OrderStatus status;

        Order(String orderId, OrderStatus status) {
            this.orderId = orderId;
            this.status = status;
        }

        @Override
        public String toString() {
            return "Order{id='" + orderId + "', status=" + status
                + "('" + status.getDbCode() + "')}";
        }
    }

    public static void main(String[] args) {
        // DB保存イメージ: Enum → コード値
        OrderStatus status = OrderStatus.PROCESSING;
        String dbValue = status.getDbCode(); // "PROC"
        System.out.println("DB保存値: " + dbValue);

        // DB読み込みイメージ: コード値 → Enum
        OrderStatus restored = OrderStatus.fromDbCode(dbValue);
        System.out.println("DB復元: " + restored);

        // name() による保存(ordinal よりは安全だが、リネームに注意)
        String nameValue = status.name(); // "PROCESSING"
        System.out.println("name(): " + nameValue);
        OrderStatus byName = OrderStatus.valueOf(nameValue);
        System.out.println("valueOf(): " + byName);

        // 全ステータスのコード値を表示
        System.out.println("\n=== ステータス一覧 ===");
        for (OrderStatus s : OrderStatus.values()) {
            System.out.println(s.name() + " → dbCode=" + s.getDbCode());
        }

        Order order = new Order("ORD-001", OrderStatus.COMPLETED);
        System.out.println("\n注文: " + order);
    }
}

Java 8 では fromDbCode() に for ループを使います。Enum は java.io.Serializable を実装しているので、標準シリアライズを使う場合は自動的にシリアライズできます。デシリアライズ後も同一インスタンス(シングルトン)が保証されます。

Jackson を使った JSON 変換(許可ライブラリ)

実務では Jackson を使った JSON 変換が一般的です。@JsonValue@JsonCreator アノテーションを使うことで、 シリアライズ・デシリアライズ時に固定コード値を使うよう指定できます。

Sample.java
// Jackson を使った場合(pom.xml に jackson-databind 依存追加が必要)
// Maven: <dependency>
//   <groupId>com.fasterxml.jackson.core</groupId>
//   <artifactId>jackson-databind</artifactId>
//   <version>2.17.0</version>
// </dependency>

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.ObjectMapper;

enum OrderStatusJson {
    PENDING("PEND"),
    PROCESSING("PROC"),
    COMPLETED("COMP"),
    CANCELLED("CANC");

    private final String dbCode;

    OrderStatusJson(String dbCode) {
        this.dbCode = dbCode;
    }

    // JSON に変換するときはこのメソッドの値を使う
    @JsonValue
    public String getDbCode() { return dbCode; }

    // JSON から変換するときはこのメソッドを使う
    @JsonCreator
    public static OrderStatusJson fromDbCode(String code) {
        for (OrderStatusJson s : values()) {
            if (s.dbCode.equals(code)) return s;
        }
        throw new IllegalArgumentException("不明なコード: " + code);
    }
}

// 使用例
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(OrderStatusJson.PROCESSING); // "PROC"
OrderStatusJson status = mapper.readValue("\"PROC\"", OrderStatusJson.class); // PROCESSING

Jackson を使わない場合は、Pure Java でも getDbCode()fromDbCode() で同等の変換を実装できます。

よくあるミス・注意点

ordinal() を永続化に使ってはいけない

ordinal() は定義順の 0 始まりの整数です。 Enum に新しい定数を途中に挿入したり、定義順を変更したりすると値がずれて、 既存の DB データや JSON の意味が変わってしまいます。 永続化には必ず明示的なコード値か name() を使いましょう。

name() による保存は定数のリネームに弱い

name() は定数名の文字列(例: "PROCESSING")を返します。 ordinal より安全ですが、定数名をリファクタリングで変更すると既存データと一致しなくなります。 長期運用するシステムでは固定コード値の方が安全です。

Jackson のデフォルトは name() で変換する

Jackson は設定なしだと Enum をname()("PROCESSING" など)でシリアライズします。 固定コード値("PROC" など)を使いたい場合は@JsonValue@JsonCreator を明示的に付けましょう。

Enum の標準シリアライズは readResolve() が不要

通常のクラスをシリアライズしてシングルトンを保つにはreadResolve() が必要ですが、 Enum は JVM レベルでシングルトンが保証されており、デシリアライズ後も同一インスタンスが返ります。

テストする観点

  • getDbCode() が各定数の期待するコード値("PEND", "PROC" など)を返すこと
  • fromDbCode() に各定数のコード値を渡したとき、対応する Enum が返ること(正常系・全件)
  • fromDbCode() に存在しないコード値を渡したとき IllegalArgumentException がスローされること(異常系)
  • fromDbCode() に空文字を渡したとき IllegalArgumentException がスローされること(境界値)
  • name() が定数名の文字列を返し、valueOf(name()) で元の Enum に戻せること
  • dbCode → fromDbCode → getDbCode の往復で同じコード値が得られること(ラウンドトリップ)

GitHub でソースコードを見る →