JSON Toolkit

JSON から Java クラスへ:ジェネレーターがくれるのは骨格だけ、本番までに足す 5 つのこと

要点:本サイトのジェネレーターで JSON → Java のデフォルト出力を実測——数値は Double、ネストしたオブジェクトは static 内部クラス、Jackson アノテーションは一切生成されません。金額の精度、フィールド名の対応、ジェネリクス消去、未知フィールド、日付とデフォルトコンストラクタの 5 つを、コピーして使えるコードとともに解説します。

中級約 10 分で読めます2026-11-04 公開2026-11-04 更新
JSON を Java POJO クラスコードに変換するツールの画面

結論:ジェネレーターが 10 秒で出すクラス構造はコンパイルは通りますが、本番にはそのまま使えません。本サイト实测のデフォルト出力は number→Double、ネストしたオブジェクト→static 内部クラス、Jackson アノテーションなし。金額の精度、フィールド名の対応、ジェネリクス消去、未知フィールド、日付——この 5 つは一切やってくれません。

API レスポンスを Java クラスにするのは純粋な機械作業です。JSON を見てフィールドを写し、getter/setter を付ける。機械作業の代償は見えないナイフです——プロパティ名を一文字写し間違えてもコンパイルは通らず、フィールドは黙って null のまま、気づくのは本番ログの中です。クラス構造を生成するのは、初日から名前と型を揃えるため。ただし揃えられるのは「サンプルから見える部分」だけです。

1. まずジェネレーターを回す:実際に何が出るか

代表値を含む API レスポンスを本サイトの JSON → Java ツールページに貼ります:

{
  "order_id": 202611040001,
  "buyer": {
    "name": "王五",
    "vip": true
  },
  "items": [
    { "sku": "A-1", "price": 19.99, "qty": 2 }
  ],
  "created_at": "2026-11-04T09:30:00+08:00"
}

生成された骨格(デフォルト、useLombok オフ):

public class Root {
    private Double orderId;
    private Buyer buyer;
    private List<Item> items;
    private String createdAt;

    public Double getOrderId() { return orderId; }
    public void setOrderId(Double orderId) { this.orderId = orderId; }
    // ...残りの getter/setter も同様
}

Buyer と Item は独立した static 内部クラスです。マッピング規則を一つずつ確認します:

JSON の形 生成される Java 型 備考
"name": "王五" String 文字列は常に String
"qty": 2 Double 数値はすべて Double、2 節参照
"vip": true Boolean ラッパー型、null 許容
"buyer": {...} Buyer(static 内部クラス) ネストしたオブジェクトは独立クラスに
"items": [...] List<Item> 要素型は先頭要素から推定
order_id orderId snake_case を自動で camelCase に

クラス名は PascalCase(Root)、フィールドは camelCase(orderId)。Java のキーワード(class、new、int)と衝突した場合は自動で接尾辞を付けて回避します。オプションは packageName、className、useLombok(@Data 生成か手書き getter/setter か)の 3 つ。Lombok を有効にするとボイラープレート全体が数個のアノテーションに収まります。

この骨格には 2 つの境界があります。第一に、Jackson/Gson のアノテーションを一切生成しないこと——@JsonProperty も @JsonIgnoreProperties も @JsonFormat もありません。アノテーションは「デシリアライズをどう実行するか」を決めるもので、サンプルから実行方針は推定できないからです。第二に、逆変換はないこと:Java クラスから JSON を導くのは対象外で、JSON → TypeScript Interface と同じ一方向設計です。

2. 最初に補うこと:Double は金額の型ではない

ジェネレーターは "price": 19.99 を Double にします。コンパイルも代入も問題ないのに、この型にお金を入れてはいけません。2 進浮動小数点は 0.1 を表現できず、ほとんどの十進小数も正確に表せません:

System.out.println(19.99);          // 19.99 —— 問題なさそうに見える
double a = 0.1 + 0.2;
System.out.println(a);              // 0.30000000000000004
System.out.println(a == 0.3);       // false

19.99 を単独で表示する場合は最短表現の整形が働くので問題が見えません。演算に入った瞬間に誤差が顔を出します。英語圏の 3 つのチュートリアルがこの点で完全に一致しています:価格・金額・料率はすべて BigDecimal に変更する。さらに構築方法を new BigDecimal("19.99")(文字列コンストラクタ)と決めること。new BigDecimal(19.99) にすると double の誤差がそのまま入ります。

機械的な補い方は生成後の置換です:金額フィールドに限り Double → BigDecimal。同根の問題も一言:2^53 を超える整数(注文番号、Snowflake ID)を Double で受ける 下位桁が黙って丸められ、20 桁の数字の末尾 3 桁が消えるのは実測でも起きます。仕組みは JSON の巨大整数が精度を失う理由と同じです。こういうフィールドは String か Long にして、数字をテキストとして運ばせてください。

フィールドの性質 ジェネレーターの出力 変更先
金額・料率 Double BigDecimal(文字列コンストラクタ)
注文番号・ID(2^53 超の可能性) Double String か Long
数量・ページ番号などの整数 Double Integer / Long
本当に科学計算に使う浮動小数 Double そのまま

3. 2 つ目:名前の不一致は @JsonProperty で固定

order_id を orderId にするのは「善意の推測」です。Java 側はキャメルケースがいいだろうという仮定。しかし Jackson はデフォルトで JSON キーとフィールド名を完全一致で照合し、合わなくてもエラーにはならず orderId は null のままです。修正は 2 通り:

フィールド単位——@JsonProperty を 1 つ付ければ 1 つの対応が固定されます:

public class Root {
    @JsonProperty("order_id")
    private Long orderId;
}

全体単位——ObjectMapper の命名戦略を変えれば、プロジェクト全体でフィールドごとのアノテーションが不要になります:

ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

どちらを選ぶかは接続先の数で決めます。固定の 1 つの API だけならフィールド単位が明快。チーム全体が snake_case の外部サービスと付き合うなら全体戦略。ここで強調したい裏面の事実——名前を書き間違えた失敗は「静かな null」であって例外ではないということです。jsonformater のツールページはこれをジェネレーターが存在する理由としています。機械的な写し間違いが最も起きやすいのがまさにこの工程だからです。アノテーションを補ったら、少なくとも主要フィールドが null でないことを確認するアサーションテストを 1 つ書きましょう。

4. 3 つ目:型消去——List を裸でデシリアライズしない

ジェネレーターは List<Item> のフィールド宣言をくれますが、JSON 配列をその List に流し込むのは Jackson です。トップレベルでこう書くと型消去の罠に落ちます:

// 誤り:実行時に取得できる要素は LinkedHashMap
List<Item> items = mapper.readValue(json, List.class);

// 正しい:TypeReference がジェネリクス引数を保存する
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

readValue(json, List.class) はコンパイルも実行もできます。しかし Java のジェネリクスはコンパイル時にしか存在せず、実行時の Jackson に見えるのは List だけ。要素は LinkedHashMap に落ちます。エラーはすぐには発生せず、どこか別の場所の item.getSku() で初めて ClassCastException が飛びます。発生位置とエラー報告位置が大きく離れるのが特徴です。正しい書き方を 3 つ、場面別に:

// 1. トップレベルが配列
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

// 2. トップレベルオブジェクトのフィールド
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});

// 3. JavaType を明示的に構築
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Item.class);
List<Item> items = mapper.readValue(json, type);

Gson なら TypeToken が同じ役割です。3 つのチュートリアルすべてが独立した項目として挙げている落とし穴です。失敗が遅く、報告が遠いからです。

5. 4 つ目:未知フィールド——API の進化でデシリアライズを壊さない

API にフィールドが 1 つ増えるのはごく普通の進化です。ところが Jackson のデフォルトは未知フィールドで UnrecognizedPropertyException を投げる——相手がフィールドを追加しただけで、こちらのサービスがエラーを出し始めます。2 つの層で無効化します:

// クラス単位:このクラスの未知フィールドだけ許容
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }

// 全体:mapper 全体で許容
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

無効化すると鏡像の問題も現れます:許容すると、フィールド名の書き間違いも報告されなくなる。前節の「静かな null」が全方位に通じるようになります。折衷案は、本番は許容オフ、テストは許容オンで主要 API を一巡させること。綴りミスはテスト期間に露見させます。Gson はデフォルトで未知フィールドを無視します。素早い接続で楽な理由がこれで、その代償は相手が何を追加したか永遠に分からないことです。

6. 5 つ目:日付とデフォルトコンストラクタ

created_at は String にマップされますが、実はこれが最も安全なデフォルトです。JSON の日付は文字列で、形式は ISO-8601、Unix タイムスタンプ、独自形式の 3 态があり、サンプルからはどれが欲しいか推定できません。本物の日付型が必要になったら 2 行足します:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// シリアライズ時にタイムスタンプ配列ではなく ISO-8601 で出力
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;
}

デフォルトコンストラクタも必須確認項目です。Jackson はデフォルトコンストラクタでオブジェクトを生成してからフィールドを順に set します。引数付きコンストラクタを書くとデフォルトが消え、デシリアライズは即座に失敗します。Lombok なら @NoArgsConstructor を補う。Java 17 の record を使うなら Jackson 2.12+ の record 対応に任せて、コンストラクタ引数でマッチするのでこの要件自体が消えます。

7. 本番の 2 つの論点:ObjectMapper の再利用と入力検証

ジェネレーターはクラスをくれますが、実行時にはアーキテクチャの話がもう 2 つあります。第一に、ObjectMapper は生成コストが高くスレッドセーフなので、アプリ全体で 1 つのインスタンスを共有すること。メソッド内の new ObjectMapper() は最も一般的な性能アンチパターンで、Spring Boot がデフォルトで注入するインスタンスはまさにこのためのものです。命名戦略、JavaTimeModule、未知フィールドの方針は全部この 1 インスタンスに集約し、あちこちに散らさないこと。

第二に、デシリアライズの成功は「型が合った」ことだけを示し、値が妥当であることは示しません。マイナスの価格、0 の数量、形式の不正なメールアドレス——Jackson は関知しません。入力検証は Bean Validation(hibernate-validator)に任せ、生成された骨格にアノテーションを補います:

public class CreateOrderRequest {
    @NotNull
    @DecimalMin(value = "0.01")
    private BigDecimal price;

    @Min(1)
    private Integer qty;

    @Email
    private String contact;
}

Spring なら @Valid を付けるだけでビジネスロジックの手前で弾けます。この層はジェネレーターが永遠に提供しないものです。サンプルから読み取れるのは「フィールドの見た目」であって「このフィールドの業務上の制約」ではないからです。

8. 生成と手書きの境界

いつジェネレーターを使い、いつ手で書くか。境界線は意外とはっきりしています:

  • ジェネレーターを使う場面:フィールド数の多いレスポンス、一度きりのサードパーティ API 対応、素早いプロトタイプ。消滅させるのは「機械的な写し間違い」という 1 種類のエラーだけで、名前と型を揃えた骨格を 10 秒で手に入ります。
  • 手書きまたは深い改造が必要な場面:金額と長整数の型変更、命名戦略の全体決定、コア領域オブジェクトへの検証アノテーション追加。これらは業務上の判断で、サンプルからは出てきません。
  • 両者に共通する前提:元の JSON が妥当であることを先に確認する。ジェネレーターは壊れた入力でもそのまま生成し、罠をデシリアライズ時に残します。ツールページに貼る前に本サイトの検証ページを通しましょう。どちらのみ 10 秒です。

ジェネレーターは「0 から骨格まで」を解決し、本稿の 5 つは「骨格から本番まで」を解決します。順序は逆にできず、省略もできません。

よくある質問

生成されたクラスを Spring Boot でそのまま使えますか?

コンパイルは通ります。実行前に 3 つ補うことを勧めます:金額フィールドを BigDecimal に、snake_case の API 向けに命名戦略か @JsonProperty を、そして @JsonIgnoreProperties(ignoreUnknown = true) で API 進化に備える。Bean Validation のアノテーションは業務に合わせて。

なぜ数値がすべて Double になり、long や BigDecimal ではないのですか?

サンプルは「整数か小数か」は区別できても業務上の意味は区別できません。2 は数量かもしれないし番号かもしれない。20 桁の注文番号も 19.99 円も JSON ではどちらも number です。意味の判断を利用者に委ねるのは前向き生成器として妥当な境界で、代償として 2 節の置換表を自分で通す必要があります。

Lombok の @Data と手書き getter/setter、どちらを選ぶべき?

チームがすでに Lombok を使っているなら @Data。ボイラープレートが最も減ります。コンパイル時プラグインや他のアノテーションプロセッサとの競合を避けたいなら手書き。Jackson にとっての違いはゼロで、getter/setter かフィールドの可視性しか見ません。

ネストしたオブジェクトが static 内部クラスになるのは問題?

Jackson には影響ありません。static 内部クラスは独立してインスタンス化できます。ただし長期保守では、独立したトップレベルクラスとしてパッケージに切り出すのが一般的です。再利用と単体テストのためです。ジェネレーターの内部クラス構造は素早い確認向けで、育てるコードベース向けではありません。

List フィールドのデシリアライズで ClassCastException が出たら?

まず裸の List.class でデシリアライズしていないか確認します。要素は実際には LinkedHashMap で、フィールドを取り出す時点で爆発します。TypeReference に変えても出るなら、本サイトのエラーメッセージ逆引きページに例外を貼って言語別の該当項目を確認してください。