概要

API 連携やファイル保存で、record で定義した DTO をシリアライズする場面は少なくありません。record は Serializable を implements すればバイナリ直列化に対応しますが、従来のクラスとは復元の仕組みが異なる点があります。また、現場では JSON 形式でのやり取りが主流であり、Jackson や Gson なしで最低限の JSON 変換を Pure Java で行いたいケースもあります。この記事では、record の標準シリアライズ(ObjectOutputStream / ObjectInputStream)の挙動と注意点、手動 JSON 変換の実装パターン、そして Java 21 で sealed interface と組み合わせた型安全な JSON 生成までを扱います。外部ライブラリを使わずに動く完結したコードを示しつつ、実務で Jackson に移行すべきラインについても触れます。

使いどころ

マイクロサービス間の HTTP レスポンスを record で受け取り、手動 JSON 変換でログ出力する

バッチ処理の中間結果を record のバイナリシリアライズで一時ファイルに保存し、障害時にリトライする

設定ファイルの読み込み結果を record に詰めて返却し、変更不可の設定値オブジェクトとして扱う

コード例

RecordSerializeDemo.java
import java.io.*;

public class RecordSerializeDemo {

    // Serializable を明示的に implements
    record UserDto(String id, String name, int age) implements Serializable {
        @SuppressWarnings("unused")
        private static final long serialVersionUID = 1L;
    }

    // Pure Java での手動 JSON 変換
    static String toJson(UserDto user) {
        return "{\"id\":\"" + user.id() + "\","
                + "\"name\":\"" + user.name() + "\","
                + "\"age\":" + user.age() + "}";
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        var user = new UserDto("U001", "田中太郎", 30);
        System.out.println("元オブジェクト: " + user);

        var json = toJson(user);
        System.out.println("JSON: " + json);

        byte[] bytes;
        try (var baos = new ByteArrayOutputStream();
             var oos = new ObjectOutputStream(baos)) {
            oos.writeObject(user);
            bytes = baos.toByteArray();
        }
        System.out.println("シリアライズ済みバイト数: " + bytes.length);

        try (var bais = new ByteArrayInputStream(bytes);
             var ois = new ObjectInputStream(bais)) {
            var restored = (UserDto) ois.readObject();
            System.out.println("復元: " + restored);
            System.out.println("元と一致: " + user.equals(restored));
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record に Serializable を implements すればバイナリ直列化が可能。アクセサが name() 形式なので JSON 変換のコードも簡潔に書ける。

Java 17
// Java 17: record で簡潔に定義 + JSON 変換
record UserDto(String id, String name, int age)
        implements Serializable {
    private static final long serialVersionUID = 1L;
}
static String toJson(UserDto u) {
    return "{\"id\":\"" + u.id() + "\","
         + "\"name\":\"" + u.name() + "\","
         + "\"age\":" + u.age() + "}";
}

Library Comparison

標準 API(手動 JSON / Serializable)フィールドが少なく構造が単純な DTO で、外部依存を入れたくない場合。ログ出力やデバッグ用途。エスケープ処理やネスト構造の対応は手動で行う必要があり、フィールド数が増えると保守が難しくなる。
Jackson(ObjectMapper)本番の API 連携や複雑な JSON 構造を扱う場合。record とも自然に連携する。依存の追加が必要。バージョンアップ時に record 対応のモジュール構成が変わることがある。
Gson(Google)Android 開発や軽量な JSON ライブラリが必要な場合。record のデフォルトコンストラクタが無いためデシリアライズに追加設定が要る場合がある。Jackson と比較してメンテナンス頻度がやや低い。

注意点

record は Serializable を自動で implements しない。バイナリ直列化が必要な場合は明示的に implements Serializable を付ける

record のデシリアライズはコンストラクタ経由で行われるため、従来クラスの readObject / writeObject とは復元メカニズムが異なる。カスタムシリアライズは基本的に不要

serialVersionUID は record でも定義を推奨する。フィールドの追加・削除時に互換性エラーを明示的に検知できる

手動 JSON 変換で文字列フィールドにダブルクォートやバックスラッシュが含まれる場合、エスケープ処理が必要になる。本番コードでは Jackson の導入を検討すべきライン

record を JSON に変換する際、null フィールドの扱い(省略するか null として出すか)を事前に決めておかないと、受け取り側でパースエラーになることがある

FAQ

record でバイナリシリアライズを使うべき場面はありますか。

一時ファイルへの中間保存やキャッシュなど、Java 同士でしかやり取りしない場面では有効です。ただし外部システムとの連携では JSON や Protocol Buffers のほうが互換性・可読性の面で有利です。

record の JSON 変換で null フィールドはどう扱うのが安全ですか。

API 仕様で null の扱いを決めておくのが前提です。手動変換では null チェックを入れて省略または "null" を出力します。Jackson なら @JsonInclude で制御できます。

record のシリアライズで serialVersionUID は必須ですか。

必須ではありませんが、定義しておくとフィールド構成の変更時に InvalidClassException で早期に検知できます。永続化やキャッシュに使う record では付けておくことを推奨します。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます