Spring Boot 3 系で動いていた JSON のデシリアライズが、Boot 4 に上げると次の例外で止まります。
Exception in thread "main" tools.jackson.databind.exc.MismatchedInputException: Cannot map `null` into type `int` (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)
at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #UNKNOWN] (through reference chain: Main$Employee["id"])
フィールド定義(int id)も JSON も変えていません。例外を投げているのは Boot 4 が管理する
Jackson 3(package が tools.jackson になった世代)で、JSON の null を int などのプリミティブ型フィールドに
入れようとした地点で止まっています。なお Boot 4 への更新では、この例外の手前で ObjectMapper の import 自体が
コンパイルエラーになる境界も越えます(そちらは
package com.fasterxml.jackson.databind does not exist の直し方)。
この記事は import を新名前空間へ直した後に残る、実行時の挙動の境界を扱います。
直し方の要点を先に
フィールドを int から Integer に変えます。 null は null のまま入ります。
public class Employee {
public Integer id; // 旧: public int id;
public String name;
}
実測では、{"id":null,"name":"x"} を読むコードがこの1行の変更で例外なく通り、id には null が入ります
(依存も JSON も同一のまま)。null になった値をどう扱うかは、その先のコードで明示的に決めます。
エラー文が勧める設定変更は、null を黙って 0 に変える
例外メッセージは set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow
と、自分で回避設定を案内してきます。この設定も実測しました。
ObjectMapper mapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
.build();
例外は消えて終了コード 0 になりますが、id には 0 が入ります。実測の出力は id=0 の1行だけで、
null を 0 に変換したことを示す警告は出ていません。JSON が運んできた「値が無い」という情報は、この時点で
「値は 0」という別の情報に置き換わっています。id=0 が正当な値になりうるフィールドでは、両者を後から
区別する手段がありません。
つまりこの設定は「エラーを消す」のではなく、Jackson 2 までの既定の変換に戻す操作です。それがどういう 変換だったかは、次の版境界の実測に出ています。
上のスニペットは自前で mapper を組んでいる場合の書き方です。Boot が自動構成する mapper に対しては、
JsonMapperBuilderCustomizer で個別に設定するか、既定一式を Jackson 2 相当へ揃える
spring.jackson.use-jackson2-defaults プロパティが用意されています(Spring 公式ブログが Jackson 3 対応の
移行手段として案内しています)。この検査の既定も Jackson 2→3 で変わった既定の1つなので、Jackson 2 相当へ
揃えれば null は再び 0 になります——戻る先が同じ「警告なしの変換」であることは変わりません。
なぜ Spring Boot 4 で出るのか:Jackson 3 が null→0 の変換を既定で拒否する
同じフィールド定義(int id)に同一の JSON {"id":null,"name":"x"} を与えた実測です。import と mapper の
生成コードは各世代の名前空間(com.fasterxml.jackson / tools.jackson)に合わせて書き分けています——
そこを同じにできないこと自体が姉妹記事の境界で、フィールド定義・JSON・読み取り処理はどちらも同一です。
| 依存 | Jackson databind | 結果 |
|---|---|---|
Spring Boot 3.4.1 の spring-boot-starter-json | com.fasterxml.jackson.core:jackson-databind:2.18.2 | 成功(終了コード 0)・id=0 |
Spring Boot 4.0.0 の spring-boot-starter-json | tools.jackson.core:jackson-databind:3.0.2 | MismatchedInputException(終了コード 1) |
境界は FAIL_ON_NULL_FOR_PRIMITIVES の既定値です。Jackson 2 では既定で無効=JSON の null はプリミティブの
既定値(int なら 0)に変換されていました。Jackson 3 は既定で有効=同じ入力を例外にします。
Spring Boot 4 は Jackson 3 世代の依存セットへ切り替わるため、Boot の版を上げただけでこの境界を越えます。
1行目の実測が示すとおり、Boot 3 系でこのフィールドは null を 0 に変換されながら成功していました。実測では
変換は例外を出さず、出力に痕跡も残さないので、id=0 がそのまま後段(DB への保存など)へ流れます。Boot 4 の
例外は、この変換が起きる地点を実行時に見える形にしたものです。例外を消す前に決めるのは、そこに null が
来るのは仕様どおりなのか(Integer で受けて明示的に扱う)、来ないはずなのか(入力側の不備)です。
切り分け(うまくいかないとき)
- 同じ Boot 4 への更新で、コンパイルが
package com.fasterxml.jackson.databind does not existで止まる: それはこの例外の手前にある別の境界です。Jackson 3 は Maven 座標と package もtools.jacksonへ改名しています (annotations だけは旧 package のまま)。 package com.fasterxml.jackson.databind does not exist の直し方 を参照してください。 Integerに変えたら、その先でNullPointerExceptionが出るようになった:nullがフィールドまで届くように なったので、null を前提にしていなかったコードに到達しています。例外が出ていた地点=null が発生する地点なので、 そこで null の扱い(既定値に倒す・拒否する・そのまま持ち回る)をコードとして書きます。FAIL_ON_NULL_FOR_PRIMITIVESを無効にして戻すと、この決定が再び暗黙の0に置き換わります。- 例外の package が
com.fasterxml.jackson.databind.excになっている:それは Jackson 2 系が投げています。 この記事の境界(Jackson 3 への更新で既定値が変わる)とは踏み方が違います。例外を投げた側の package 名 (tools.jacksonかcom.fasterxml.jacksonか)が、どちらの世代を相手にしているかの手がかりになります。 idがlong/boolean/doubleなどでも同じ例外が出る:実測したのはintですが、feature の定義が 対象を Java のプリミティブ型全般としています。直し方は同じで、対応するボックス型 (Long/Boolean/Double)に変えます。
検証環境
maven:3.9-eclipse-temurin-21(digest 固定)の中で Maven を実行(依存の取得にのみネットワークを使用)- 依存は
spring-boot-starter-jsonのみ(Spring MVC もサーバも使わない最小構成。実アプリで踏む地点は リクエストボディの変換など Boot が自動構成する mapper 側で、その設定手段は本文に書いたとおり) - 対照(Boot 3.4.1)と再現(Boot 4.0.0)で同一なのはフィールド定義・JSON・読み取り処理。import と mapper の 生成コードは各世代の名前空間に合わせて書き分けている(同一ソースのまま両世代ではコンパイルできない=姉妹記事の境界)
- 再現:Spring Boot 4.0.0(
tools.jackson.core:jackson-databind:3.0.2)で{"id":null,"name":"x"}をint idのクラスにreadValueすると、上記の例外で終了コード 1 - 修正:フィールドを
Integerに変えると終了コード 0・シグネチャ消滅・id=null
再現から修正までは errfix の検証ハーネスが機械的に確認しています。reproduce と fix の差はフィールドの型
1箇所だけです。「Boot 3.4.1 では id=0 で成功する」「FAIL_ON_NULL_FOR_PRIMITIVES を無効にすると Boot 4.0.0 でも
id=0 で成功する」の2点は、同一 JSON で実測しました(各結果はケースの verification/probes.txt に記録)。
Boot と Jackson の版の対応は mvn dependency:tree で確認しています。