Error [tools.jackson.databind.exc.MismatchedInputException: Cannot map `null` into type `int`]

Cannot map `null` into type `int` の直し方(Spring Boot 4 / Jackson 3 で null が例外になる)

FIX SUMMARY verified
Applies when
maven@sha256:2b4496088e7b80ae10a8c9f74e574ea21380325a006ec684532ad6bad5bc7273spring-boot-starter-json (Spring Boot 4 manages tools.jackson.core:jackson-databind:3.0.2, where FAIL_ON_NULL_FOR_PRIMITIVES defaults to enabled)upgrading Spring Boot 3.x to 4.0.0 with a primitive int field receiving JSON null — same code and JSON succeed on Boot 3.4.1 by silently converting null to 0

Verified: reproduced in maven@sha256:2b4496088e7b80ae10a8c9f74e574ea21380325a006ec684532ad6bad5bc7273, then the tools.jackson.databind.exc.MismatchedInputException: Cannot map `null` into type `int` signature was gone after the fix (exit 0).

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 の nullint などのプリミティブ型フィールドに 入れようとした地点で止まっています。なお 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行だけで、 null0 に変換したことを示す警告は出ていません。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.1spring-boot-starter-jsoncom.fasterxml.jackson.core:jackson-databind:2.18.2成功(終了コード 0)・id=0
Spring Boot 4.0.0spring-boot-starter-jsontools.jackson.core:jackson-databind:3.0.2MismatchedInputException(終了コード 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.jacksoncom.fasterxml.jackson か)が、どちらの世代を相手にしているかの手がかりになります。
  • idlong / 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 で確認しています。

検証(machine-verified)

この修正は maven@sha256:2b4496088e7b80ae10a8c9f74e574ea21380325a006ec684532ad6bad5bc7273 のバージョン固定コンテナ内で再現し、修正後に tools.jackson.databind.exc.MismatchedInputException: Cannot map `null` into type `int` のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs jvm/jackson3-null-primitive
● reproduce tools.jackson.databind.exc.MismatchedInputException: Cannot map `null` into type `int` present ✓
● apply fix exit 0
● re-run tools.jackson.databind.exc.MismatchedInputException: Cannot map `null` into type `int` gone ✓
PASS verified · maven@sha256:2b4496088e7b80ae10a8c9f74e574ea21380325a006ec684532ad6bad5bc7273 · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。