テックブログ

Spring Boot 4のJackson 3移行:パッケージ変更とJSON差分への対応

Spring Boot 4のJackson 3移行:パッケージ変更とJSON差分への対応

Spring Boot 4ではJackson 3が標準のJSONライブラリとなり、spring-boot-starter-jsonから利用される自動構成もJackson 3を中心に動きます。最大の見た目の変化は、依存関係のgroupIdとJavaパッケージがcom.fasterxml.jacksonからtools.jacksonへ変わったことです。ただしjackson-annotationsだけは互換性のためcom.fasterxml.jackson.coreとcom.fasterxml.jackson.annotationを維持します。単純な一括置換では、注釈まで誤って変更するおそれがあります。

移行で確認すべきなのはコンパイルエラーだけではありません。Jackson 3ではJsonMapperをビルダーで構築する設計、変更されたクラス名、読み書きの既定値、例外体系、日時処理などに差があります。アプリが起動しても、JSONレスポンス、外部APIへの送信内容、メッセージ、キャッシュ、監査ログが変われば互換性問題になります。本記事ではSpring Boot 4.1.2とJackson 3.1系を基準に、依存とパッケージの移行、Mapper設定、JSON差分テスト、Jackson 2を一時利用する場合の境界と終了条件を整理します。

1. Spring Boot 4とJackson 3の関係を整理する

1.1 Jackson 3が既定のJSONライブラリになった

Spring Boot 4はJackson 3を推奨かつ既定のJackson実装として扱います。Jackson 3がクラスパスにあると、Spring BootはJsonMapper Beanを自動構成し、Spring MVCやHTTPクライアントなどのJSON変換へ利用します。

Jackson 2のサポートも移行期間のため残っていますが、非推奨です。Spring Boot公式ドキュメントは、Jackson 2のサポートが将来のSpring Boot 4.xで削除される予定であり、長期的に依存しないよう説明しています。新しいコードをJackson 2前提で増やすのではなく、外部ライブラリの対応待ちなど、期限と理由がある場合だけ一時利用します。

Spring Bootのバージョン管理を利用しているアプリでは、Jacksonの個別バージョンを直接固定しないことが基本です。Spring Boot 4.1.2は対応するJackson BOMを管理しています。特定モジュールだけ別バージョンへ上書きすると、メソッドやクラスの不一致が起きる可能性があります。

1.2 Jackson 2と3は別パッケージで共存できる

Jackson 3は、Jackson 2と異なるgroupIdとJavaパッケージを使います。このため、同じクラスパス上に2系と3系を置ける設計です。既存ライブラリがJackson 2を必要とし、アプリ本体がJackson 3を使う段階移行も技術的には可能です。

ただし、共存できることと、自動的に相互運用できることは別です。Jackson 2のcom.fasterxml.jackson.databind.ObjectMapperとJackson 3のtools.jackson.databind.json.JsonMapperは異なる型です。片方のModule、JsonNode、Serializerをもう片方へ渡すことはできません。

Bean名をobjectMapperのままにしたり、型を曖昧にしたりすると、どちらがHTTP変換に使われるか分かりにくくなります。共存期間は利用箇所をライブラリ境界へ限定し、jackson2MapperとjsonMapperのように役割が分かる名前を付けます。

1.3 JSONはアプリ外部との契約である

JSONの差分は、Java内部のリファクタリングより影響が広い場合があります。Web APIの利用者、メッセージキューの消費者、保存済みJSON、キャッシュ、署名対象、監査システムは、フィールド名、日時、数値、null、順序などを前提にしている可能性があります。

JSONオブジェクトのプロパティ順は仕様上の意味を持たないのが一般的ですが、文字列比較、スナップショット、署名、ハッシュに使っているシステムでは差分になります。移行前に「意味が同じならよい項目」と「バイト列まで一致が必要な項目」を分けます。

互換性の基準を決めずにテストを更新すると、意図しない変更まで新しい正解として受け入れてしまいます。3.5系で代表入力と出力を保存し、4.xで差分の理由を一つずつ説明できる状態にします。

2. 依存関係とimportを移行する

2.1 groupIdとJavaパッケージを変更する

Jackson 3では、主要モジュールのgroupIdとJavaパッケージがtools.jacksonへ変わりました。たとえばdatabindの型は次のように移ります。

// Jackson 2
import com.fasterxml.jackson.databind.ObjectMapper;

// Jackson 3
import tools.jackson.databind.json.JsonMapper;

MavenでSpring Bootのstarterを使う場合は、アプリ側でJackson databindのバージョンを直接指定する必要は通常ありません。spring-boot-starter-jsonやWeb系starterが対応する依存を持ちます。直接Jacksonモジュールを追加している場合は、Spring Bootのdependency managementに含まれるJackson 3側のartifactを選びます。

置換後は./mvnw dependency:treeまたは./gradlew dependencies --configuration runtimeClasspathで、Jackson 2と3の両方が入っていないか確認します。両方がある場合は、どの依存がJackson 2を要求しているかを特定し、意図した共存かを判断します。

2.2 jackson-annotationsは例外として残す

jackson-annotationsは例外です。Jackson 3でもgroupIdはcom.fasterxml.jackson.core、パッケージはcom.fasterxml.jackson.annotationを維持します。

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;

IDEの一括置換でcom.fasterxml.jacksonをすべてtools.jacksonへ変えると、存在しない注釈パッケージを作ってしまいます。対象をcom.fasterxml.jackson.core、com.fasterxml.jackson.databind、com.fasterxml.jackson.dataformatなどに分け、注釈を除外します。

独自ライブラリが公開APIでJacksonの型を受け渡している場合、パッケージ変更は破壊的変更です。アプリだけでなく、共通ライブラリのメジャーバージョンや利用側の更新計画も必要になります。

2.3 Java 8向け追加モジュールを棚卸しする

Jackson 3では、Java 8世代の一部機能がdatabindへ統合されました。Jackson 2で使っていたjackson-datatype-jsr310、jackson-datatype-jdk8、jackson-module-parameter-namesなどは、依存や手動登録が不要になる場合があります。

// Jackson 2でよく見られた例
objectMapper.registerModule(new JavaTimeModule());

移行では、パッケージ名だけ直して同じModuleを登録し直すのではなく、Jackson 3の移行ガイドで組み込み状況を確認します。不要なモジュールを残すとクラスが見つからない、同じ機能が二重登録される、古いJackson 2が推移的に入るといった問題につながります。

日時の出力形式はモジュール有無だけで決まらないため、実際のJSONをテストします。LocalDateTime、Instant、OffsetDateTime、タイムゾーン付き値を代表ケースとして固定します。

3. JsonMapperの構築方法を変更する

3.1 Spring Bootが自動構成するJsonMapperを優先する

Spring Bootアプリでは、まず自動構成されたJsonMapperを利用します。アプリの各所でnew JsonMapper()を作ると、Spring Bootの設定、Module、Customizerが反映されず、HTTPレスポンスとバッチ処理で異なるJSONが生成される可能性があります。

@Service
class JsonExportService {
    private final JsonMapper jsonMapper;

    JsonExportService(JsonMapper jsonMapper) {
        this.jsonMapper = jsonMapper;
    }
}

MapperをDIすると、アプリ全体のJSON方針を一か所で管理できます。ただし、外部サービスごとに契約が異なる場合は、用途別のObjectReaderやObjectWriter、専用Mapperを明示的に作る方が安全です。

3.2 Builderで設定して不変なMapperを作る

Jackson 3のObjectMapperとJsonFactoryは不変として設計され、構築後に設定を追加するJackson 2の書き方から、Builderで設定して完成させる書き方へ移ります。

JsonMapper mapper = JsonMapper.builder()
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .enable(SerializationFeature.INDENT_OUTPUT)
        .build();

既存Mapperを基に別設定のMapperを作る場合は、rebuild()からBuilderを得ます。

JsonMapper prettyMapper = jsonMapper.rebuild()
        .enable(SerializationFeature.INDENT_OUTPUT)
        .build();

実行中に共有Mapperの設定が変わらないため、スレッド間で設定変更が競合する問題を避けやすくなります。一方、起動後にregisterModuleやconfigureを呼ぶコードはコンパイルエラーまたは設計変更の対象になります。

3.3 JsonMapperBuilderCustomizerへ置き換える

Spring Bootの自動構成をカスタマイズする場合、Jackson2ObjectMapperBuilderCustomizerはJsonMapperBuilderCustomizerへ名称が変わりました。カスタマイザーをBeanとして登録し、Mapper構築前のBuilderへ設定を加えます。

@Configuration
class JacksonConfiguration {

    @Bean
    JsonMapperBuilderCustomizer jsonMapperCustomizer() {
        return builder -> builder
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }
}

この方法なら、Spring Bootが登録するModuleや設定プロパティと同じ構築過程へ参加できます。独自のJsonMapper Beanを丸ごと定義すると、自動構成の条件が変わり、期待していたCustomizerが適用されない場合があります。

設定の責任範囲も整理します。一般設定はspring.jackson.*、型固有の変換はSerializerやModule、外部サービス固有の差は専用Reader/Writerとするなど、どこに何を書くかを決めます。

4. Spring BootとJacksonの名称変更へ対応する

4.1 SerializerとDeserializerの型名を更新する

Jackson 3では、いくつかのクラス名が整理されています。Spring Bootの移行ガイドでは、JsonObjectSerializerがObjectValueSerializer、JsonValueDeserializerがObjectValueDeserializerへ変更されたことが案内されています。

独自Serializerでは、対象型、nullの扱い、フィールド名、例外時の動作を確認します。コンパイルが通る型へ置き換えるだけでなく、Jackson 2で出力していたJSONとの比較が必要です。

Deserializerは入力互換性へ影響します。古いクライアントが送る形式、新しい形式、欠損値、不正値をテストし、どこまで受け入れるかを決めます。移行を理由に検証を緩めると、誤ったデータを受け入れる可能性があります。

4.2 @JsonComponentと@JsonMixinの新名称を使う

Spring Boot側の注釈もJackson固有であることが分かる名称へ変わりました。@JsonComponentは@JacksonComponent、@JsonMixinは@JacksonMixinへ移行します。

これらはcom.fasterxml.jackson.annotationにあるJackson本体の注釈ではなく、Spring BootがSerializerやMixinを検出するための仕組みです。import元を確認し、同じ名前に見える別注釈と混同しないようにします。

Spring Bootはアプリのパッケージを走査して@JacksonMixinを自動構成されたJsonMapperへ登録します。パッケージ構成やComponent Scanの境界を変えた場合は、注釈を置き換えても検出されないことがあります。

4.3 設定プロパティの階層変更を反映する

JSON固有の読み書き設定は、spring.jackson.read.*とspring.jackson.write.*から、spring.jackson.json.read.*とspring.jackson.json.write.*の配下へ移りました。

spring:
  jackson:
    json:
      read:
        # JSON読み取り機能
      write:
        # JSON書き出し機能

実際の機能名は、利用するSpring Boot 4.1.2の設定リファレンスで確認します。JacksonのFeature名を小文字とハイフンへ変換すれば必ず設定できる、と推測して追加しないでください。

設定移行にはspring-boot-properties-migratorが役立ちますが、Javaコード内で直接Builderへ設定している項目までは直しません。設定ファイルとコードの両方を検索します。

5. 既定値によるJSON差分を確認する

5.1 jackson2-defaultsは一時的な比較手段として使う

Spring Boot 4には、Jackson 3の自動構成をJackson 2時代の既定値へできるだけ近づけるため、次の設定があります。

spring.jackson.use-jackson2-defaults=true

この設定は、Jackson 3へ移行しながらJSON差分を減らすための足場です。すべての差を完全に消す保証ではなく、Jackson 2そのものへ戻す設定でもありません。Spring公式の推奨順は、Jackson 3へ適応すること、必要ならこの互換既定値を使って段階移行すること、最後の手段としてJackson 2を一時利用することです。

まず設定なしで差分を取得し、次にuse-jackson2-defaults=trueで差分が減るか確認します。設定によって隠れた差分と、コード変更が必要な差分を分類し、最終的には明示的な設定とテストへ置き換えます。

5.2 読み取りの厳格化を確認する

Jackson 3では、入力末尾に余分なトークンがある場合に失敗するFAIL_ON_TRAILING_TOKENSが既定で有効です。たとえば、正常なJSONオブジェクトの後ろへ別の値が続く入力は、Jackson 2で先頭だけ読めていた場合でもJackson 3では拒否されます。

これは入力の完全性を確認する安全側の変更です。既存テストが失敗したから即座に無効化するのではなく、送信側が不正な複数値や余分なデータを送っていないか調べます。信頼できる入力で性能上の根拠がある場合だけ、影響を測定して変更します。

プリミティブ型へのnullや欠損値も確認します。Jackson 3ではFAIL_ON_NULL_FOR_PRIMITIVESが既定で有効であり、intやbooleanへnullを入れて暗黙の既定値にする入力が失敗する可能性があります。API契約として必須なら、失敗する方が問題を早く発見できます。任意項目ならラッパー型や明示的な既定値を検討します。

5.3 日時と出力形式を実データで比べる

Jackson 3では日時関連の機能が整理され、SerializationFeature.WRITE_DATES_AS_TIMESTAMPSなどの扱いも変わっています。Jackson 3移行ガイドでは、日時設定にDateTimeFeatureを使う例が示されています。

確認対象には、LocalDate、LocalDateTime、Instant、OffsetDateTime、タイムゾーン、ミリ秒以下の精度を含めます。金融や業務システムでは、タイムゾーンの欠落や精度の変化が検索条件、締め処理、署名、監査へ影響します。

日時は見た目がISO 8601でも、Zと+00:00、秒とミリ秒、オフセット有無が異なる場合があります。文字列比較だけでなく、利用側が期待する型へ復元できるかもテストします。

6. JSON差分テストを設計する

6.1 3.5系の代表JSONを基準として保存する

移行前のSpring Boot 3.5系で、代表的なDTOと入力を選び、JSONを保存します。正常系だけでなく、null、空配列、空文字、境界値、大きな数値、未知フィールド、列挙値、日時、ネスト、継承型を含めます。

すべての本番データをコピーする必要はありません。契約上意味のある形を網羅したテストフィクスチャを作ります。個人情報や機密データは匿名化し、リポジトリへ保存してよい内容だけを使います。

保存する基準には、使用したSpring BootとJacksonの版、Mapper設定、プロファイルを記録します。基準が曖昧だと、後から差分の原因を再現できません。

6.2 意味の比較と文字列比較を使い分ける

通常のJSON APIでは、オブジェクトのプロパティ順や空白は意味を持ちません。JSONツリーへ変換して意味を比較すれば、無関係な整形差分でテストが壊れるのを防げます。

一方、署名対象、キャッシュキー、監査ファイル、外部仕様で順序が決まっている出力は、文字列またはバイト列の一致が必要です。用途を分けずにすべてツリー比較にすると、重要な順序差を見逃します。

差分レポートでは、フィールド追加・削除、型変更、値変更、順序・整形変更を分けます。「スナップショットが変わった」だけでなく、利用者へ影響する差かを判断できる形にします。

6.3 HTTP・メッセージ・保存JSONを別々に検証する

同じMapper設定が使われていると思っていても、Spring MVC、WebFlux、Kafka、Redis、ファイル出力、外部SDKが別のMapperを生成している場合があります。経路ごとに実際の変換を通すテストを用意します。

HTTPではMockMvcやWebTestClientでレスポンスとリクエストを確認します。メッセージではSerializer、Deserializer、ヘッダー、型情報を確認します。保存済みJSONは、旧版で書いたデータを新版で読めるか、新版で書いたデータを必要な旧版が読めるかを試します。

ローリング更新で旧版と新版が同時稼働するなら、双方向互換性が重要です。切り替え後に一括移行するデータなら、移行スクリプトと戻す方法を準備します。

7. Jackson 2を暫定利用する場合の境界を決める

7.1 spring-boot-jackson2は期限付きの足場である

Jackson 3へ対応していないライブラリがある場合、Spring Boot 4は非推奨のspring-boot-jackson2モジュールを提供しています。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-jackson2</artifactId>
</dependency>

Gradleでは次のように追加します。

implementation("org.springframework.boot:spring-boot-jackson2")

Jackson 2向けの設定はspring.jackson2配下です。これは恒久対応ではなく、Jackson 3へ移る時間を確保するためのstop-gapです。導入時に、必要なライブラリ、対応予定版、削除期限、担当者を記録します。

7.2 共存範囲をアダプターへ閉じ込める

外部SDKだけがJackson 2を要求するなら、Jackson 2の型をそのSDKとのアダプター内に閉じ込めます。業務サービスやControllerへcom.fasterxml.jackson.databind.JsonNodeを広げると、後でJackson 3へ置き換える範囲が増えます。

境界では、Jackson固有の型ではなく、自社DTO、String、byte[]などで受け渡します。必要に応じてJackson 2で外部形式を読み、自社DTOへ変換してからJackson 3側へ渡します。

2種類のMapper Beanを登録するときは、@Qualifierで利用先を明示します。@Primaryだけに依存すると、新しいBean追加や自動構成条件の変化で意図しないMapperが注入される可能性があります。

7.3 Jackson 2を外す条件をテストへ組み込む

対応待ちライブラリを更新したら、Jackson 2への依存とspring.jackson2設定を削除します。依存ツリーでcom.fasterxml.jackson.coreやcom.fasterxml.jackson.databindが残っている場合は、注釈モジュールとの区別をしながら導入元を確認します。

Jackson 3でもjackson-annotationsはcom.fasterxml.jackson側に残るため、文字列検索だけで「Jackson 2が残っている」と判断してはいけません。artifactIdとJavaパッケージを分けて調べます。

CIに依存関係ルールを入れ、許可したモジュール以外からJackson 2 databindが入ったら失敗させる方法も有効です。暫定対応を静かに恒久化させない仕組みになります。

8. よくある移行エラーを切り分ける

8.1 ClassNotFoundExceptionはimportと依存経路を確認する

com.fasterxml.jackson.databind.ObjectMapperが見つからない場合、Jackson 3へ移行済みのクラスパスでJackson 2のimportが残っている可能性があります。まず完全修飾クラス名を確認し、アプリ本体がJackson 2を必要としているのか、単なる置換漏れかを判断します。

不足クラスを含むJARを直接追加する前に、目的のstarterと依存ツリーを確認します。Jackson 2を追加するとコンパイルは通っても、HTTP変換がJackson 3、独自処理がJackson 2という分裂した構成になることがあります。

逆にtools.jacksonが見つからない場合は、古いstarter、Spring Boot BOMのずれ、Jackson 3モジュールの不足を確認します。IDEのキャッシュだけを疑う前に、コマンドラインの依存解決結果を基準にします。

8.2 Bean競合は型と用途を明示する

独自Mapper Bean、Jackson 2 Mapper、Spring Bootの自動構成Mapperが同時にあると、NoUniqueBeanDefinitionExceptionや意図しないBean選択が起こる場合があります。

各Beanの型、名前、@Qualifier、@Primary、作成条件を一覧にします。HTTPで使うMapper、外部SDK専用Mapper、バッチ専用Writerを区別し、どの変換経路がどれを使うかテストします。

独自MapperをBeanとして定義する前に、JsonMapperBuilderCustomizerやModuleで目的を達成できないか検討します。自動構成へ参加する方が、Spring Bootの設定と整合しやすくなります。

8.3 JSON差分は設定・型・経路の順に調べる

期待と異なるJSONが出たら、最初に実際に使われたMapperを特定します。次にspring.jackson.*、Builder Customizer、Module、型の注釈、Serializerの順に確認します。

同じDTOでも、HTTPとファイル出力で違う場合は変換経路が異なる可能性が高いです。すべてのMapper設定を一度に変えるのではなく、再現用の最小テストで一つの差分を固定します。

差分を直すときは、Jackson 2の動作を無条件に正解としないことも重要です。より厳格な入力検証など、安全性や契約の明確化につながるJackson 3の変更は、利用者と調整して新しい仕様として採用できます。

9. 移行完了の条件を確認する

9.1 コードと依存関係の完了条件

コード面では、主要なJackson APIがtools.jacksonへ移行し、注釈だけが意図してcom.fasterxml.jackson.annotationに残っていることを確認します。MapperはBuilderまたはSpring Bootの自動構成で作り、起動後に共有Mapperを変更するコードをなくします。

依存関係では、Jackson 2 databindが残っている場合、その導入元と削除期限が明確であることを確認します。不要になったJava 8向けModuleや直接バージョン指定も削除します。

spring-boot-jackson2を使っていない場合は、Jackson 2の自動構成を前提とした設定やBeanが残っていないか確認します。

9.2 JSON契約の完了条件

代表的なAPI、メッセージ、保存JSONについて、3.5系と4.xの差分を取得し、すべての差分に判断記録を付けます。互換に戻した差、新仕様として採用した差、利用者の更新が必要な差を分けます。

日時、数値、null、未知フィールド、末尾トークン、列挙値、ポリモーフィック型は、正常系だけでなく異常系も確認します。入力を厳格化した場合は、返すHTTPステータスやエラーメッセージも契約に含めます。

ローリング更新を行う場合は、旧版と新版の同時稼働テストを実施します。共有キュー、キャッシュ、DBのJSON列へ両版がアクセスするなら、一方向のテストだけでは不足します。

9.3 暫定設定を削除して最終確認する

spring.jackson.use-jackson2-defaults=trueやspring-boot-jackson2を使った場合は、削除計画と完了条件を確認します。互換設定を残す判断をした場合も、どの差分を維持するためかをテスト名や設計記録へ残します。

最終状態で、クリーンビルド、全テスト、依存ツリー確認、検証環境の起動、主要APIの契約テストを実行します。IDE上だけで成功した結果ではなく、CIと本番相当のビルド成果物を基準にします。

Jackson移行は、コンパイルエラーがなくなった時点ではなく、JSONを利用するすべての境界で差分を説明でき、不要なJackson 2依存を外せた時点で完了です。

まとめ

Spring Boot 4ではJackson 3が既定となり、主要なgroupIdとJavaパッケージがcom.fasterxml.jacksonからtools.jacksonへ変わります。ただしjackson-annotationsは例外として従来のパッケージを維持するため、無条件の一括置換は避けます。

MapperはSpring Bootが自動構成するJsonMapperを基本とし、JsonMapperBuilderCustomizerやBuilderで構築します。Jackson 3の既定値によって、末尾トークン、プリミティブへのnull、日時などの挙動が変わる可能性があるため、3.5系の代表JSONと比較します。

spring.jackson.use-jackson2-defaultsとspring-boot-jackson2は段階移行の手段ですが、恒久対応ではありません。共存範囲をアダプターへ限定し、削除期限とテストを用意します。最終的には、依存、Javaコード、設定、HTTP、メッセージ、保存JSONのすべてでJackson 3への移行を確認します。

参考リンク