CLOVER🍀

That was when it all began.

Jackson Databind 3を試してみる

これは、なにをしたくて書いたもの?

Jackson 3がリリースされてからそこそこ時間が経過しましたが、そろそろちゃんと確認しておこうかなということで…。

Jackson Databindを試してみます。

Jackson 3

Jacksonといえば、JavaのJSONライブラリーとして有名ですが、その他にもXMLやCSVなど様々なデータフォーマットを
扱えます。

このJacksonが2025年10月3日に3.0.0になりました。これに伴い、2.xの頃と変わったことが多数あるようなのでまずは見てみます。

リリースノートはこちら。

Jackson Release 3.0 · FasterXML/jackson Wiki · GitHub

リリースブログはこちら。

Jackson 3.0.0 (GA) released. (October 3, 2025) | by @cowtowncoder | Medium

Jackson 3に向けてのビジョンはこちら(2021年のもの)。

Jackson 3.0 vision (Jan 2021). State of Jackson 3.0 as of early 2021 | by @cowtowncoder | Medium

マイグレーションガイドはこちら。

jackson/jackson3/MIGRATING_TO_JACKSON_3.md at main · FasterXML/jackson · GitHub

ここまでのポイントとしては以下ですね。

  • Javaのベースラインは17になった
  • MavenのグループIDおよびパッケージ名がcom.fasterxml.jacksonからtools.jacksonになった
    • jackson-annotationsだけはcom.fasterxml.jackson.annotationのまま
    • Jackson 2.x/3.xの両方でアノテーションを使えるようにするため
  • 2.xで非推奨になった機能の削除
  • 主要なクラス、メソッド、フィールドの名称変更
    • たとえばJsonStreamContextTokenStreamContextJsonLocationTokenStreamLocationJsonProcessingExceptionJacksonExceptionJsonDeserializerValueDeserializerなど
  • デフォルト値の変更
    • DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIESがデフォルトで無効になった
    • MapperFeature.USE_STD_BEAN_NAMINGがデフォルトで有効になった
  • ObjectMapperおよびJsonFactoryがイミュータブルになった
    • ObjectMapper#enableのようなことができなくなり、JsonMapperからビルダーを使い構築することになる
  • new ObjectMapper(new YAMLFactory())のような書き方は許可されず、new YAMLMapper()を使用することが強制される
  • すべての例外はRuntimeExceptionのサブクラスに変更
  • Java 8用のモジュールのjackson-databindへの組み込み
    • jackson-module-parameter-names、jackson-datatype-jdk8、jackson-datatype-jsr310モジュールは不要になった
  • jackson-module-jsonSchemaモジュールの廃止

その他、パフォーマンスに関する考慮事項

Jackson 3 Migration Guide / Performance considerations (new in 3.x)

変更内容を細かく見る場合はこちらですね。

Jackson 3 Migration Guide / Detailed Conversion Guidelines

ひとまず今回は、Jackson Databindを試してみます。

GitHub - FasterXML/jackson-databind: General data-binding package for Jackson: works on streaming API (core) implementation(s)

環境

今回の環境はこちら。

$ java --version
openjdk 25.0.2 2026-01-20
OpenJDK Runtime Environment (build 25.0.2+10-Ubuntu-124.04)
OpenJDK 64-Bit Server VM (build 25.0.2+10-Ubuntu-124.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.12 (848fbb4bf2d427b72bdb2471c22fced7ebd9a7a1)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 25.0.2, vendor: Ubuntu, runtime: /usr/lib/jvm/java-25-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-100-generic", arch: "amd64", family: "unix"

準備

Maven依存関係など。

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>tools.jackson</groupId>
                <artifactId>jackson-bom</artifactId>
                <version>3.0.4</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>6.0.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>tools.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.27.7</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

jackson-databindを使います。

そういえば、jackson-annotationsだけはグループIDが変わっていないという話でした。依存関係を見てみましょう。

$ mvn dependency:tree

確かにjackson-annotationsだけ2.xですね。

[INFO] +- tools.jackson.core:jackson-databind:jar:3.0.4:compile
[INFO] |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.20:compile
[INFO] |  \- tools.jackson.core:jackson-core:jar:3.0.4:compile

この中に含まれるパッケージ名もcom.fasterxml.jacksonのままですね。

$ jar -tvf ~/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.20/jackson-annotations-2.20.jar | grep com/fasterxml/jackson | head
     0 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/
     0 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/annotation/
   429 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/annotation/JacksonAnnotation.class
   313 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/annotation/JacksonAnnotationValue.class
   502 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/annotation/JacksonAnnotationsInside.class
  4671 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/annotation/JacksonInject$Value.class
   852 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/annotation/JacksonInject.class
   577 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/annotation/JsonAlias.class
   565 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/annotation/JsonAnyGetter.class
   582 Fri Aug 29 14:50:34 JST 2025 com/fasterxml/jackson/annotation/JsonAnySetter.class

少し脱線しました。

確認はテストコードで行うので、JUnitとAssertJ Coreを含めています。

お題

お題としてはこちらのクラスを使います。

src/main/java/org/littlewings/jackson/Person.java

package org.littlewings.jackson;

import java.util.ArrayList;
import java.util.List;

public class Person {
    private String firstName;
    private String lastName;
    private int age;

    private List<Person> friends = new ArrayList<>();

    public Person() {
    }

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public void addFriend(Person person) {
        friends.add(person);
    }

    // getter/setterは省略
}

Records版も用意しました。

src/main/java/org/littlewings/jackson/PersonRecord.java

package org.littlewings.jackson;

import java.util.List;

public record PersonRecord(
        String firstName,
        String lastName,
        int age,
        List<PersonRecord> friends
) {
}

扱うデータは、いつものように(?)サザエさんです。

Jackson Databind 3を使ってみる

それでは、Jackson Databind 3を使ってみます。といっても、基本的な使い方はJackson 2.xの頃とほとんど変わっていないので
さらっといきましょう。

テストコードの雛形。

src/test/java/org/littlewings/jackson/JacksonTest.java

package org.littlewings.jackson;

import org.junit.jupiter.api.Test;
import tools.jackson.core.JacksonException;
import tools.jackson.core.exc.UnexpectedEndOfInputException;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class JacksonTest {

    // ここにテストを書く!
}

ここが1番のポイントですが、importするパッケージが見慣れたcom.fasterxml.jacksonからtools.jacksonに変わっています。

import tools.jackson.core.JacksonException;
import tools.jackson.core.exc.UnexpectedEndOfInputException;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;

あとはそれぞれ確認していきましょう。

デシリアライズ(JSON → Javaオブジェクト)。

    @Test
    void deserialize() {
        ObjectMapper mapper = new ObjectMapper();
        Person katsuo = mapper.readValue("""
                        {
                          "firstName": "カツオ",
                          "lastName": "磯野",
                          "age": 11,
                          "friends": [
                            {
                              "firstName": "弘",
                              "lastName": "中島",
                              "age": 10
                            },
                            {
                              "firstName": "花子",
                              "lastName": "花沢",
                              "age": 11
                            },
                            {
                              "firstName": "カオリ",
                              "lastName": "大空",
                              "age": 10
                            }
                          ]
                        }
                        """,
                Person.class);

        assertThat(katsuo.getFirstName()).isEqualTo("カツオ");
        assertThat(katsuo.getLastName()).isEqualTo("磯野");
        assertThat(katsuo.getAge()).isEqualTo(11);

        assertThat(katsuo.getFriends()).hasSize(3);
        assertThat(katsuo.getFriends().getFirst().getLastName()).isEqualTo("中島");
        assertThat(katsuo.getFriends().get(1).getLastName()).isEqualTo("花沢");
        assertThat(katsuo.getFriends().getLast().getLastName()).isEqualTo("大空");
    }

慣れ親しんだ(?)ObjectMapperです。

        ObjectMapper mapper = new ObjectMapper();
        Person katsuo = mapper.readValue("""
                        {
                          ...
                        }
                        """,
                Person.class);

シリアライズ(Javaオブジェクト → JSON)。

    @Test
    void serialize() {
        Person katsuo = new Person("カツオ", "磯野", 11);
        katsuo.addFriend(new Person("弘", "中島", 10));
        katsuo.addFriend(new Person("花子", "花沢", 11));
        katsuo.addFriend(new Person("カオリ", "大空", 10));

        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(katsuo);

        assertThat(json).isEqualTo("""
                {"age":11,"firstName":"カツオ","friends":[{"age":10,"firstName":"弘","friends":[],"lastName":"中島"},{"age":11,"firstName":"花子","friends":[],"lastName":"花沢"},{"age":10,"firstName":"カオリ","friends":[],"lastName":"大空"}],"lastName":"磯野"}""");
    }

ちょっとわかりにくいので、Pretty Printします。

    @Test
    void prettyPrint() {
        Person katsuo = new Person("カツオ", "磯野", 11);
        katsuo.addFriend(new Person("弘", "中島", 10));
        katsuo.addFriend(new Person("花子", "花沢", 11));
        katsuo.addFriend(new Person("カオリ", "大空", 10));

        ObjectMapper mapper = JsonMapper
                .builder()
                .configure(SerializationFeature.INDENT_OUTPUT, true)
                .build();
        String json = mapper.writeValueAsString(katsuo);

        assertThat(json).isEqualTo("""
                {
                  "age" : 11,
                  "firstName" : "カツオ",
                  "friends" : [ {
                    "age" : 10,
                    "firstName" : "弘",
                    "friends" : [ ],
                    "lastName" : "中島"
                  }, {
                    "age" : 11,
                    "firstName" : "花子",
                    "friends" : [ ],
                    "lastName" : "花沢"
                  }, {
                    "age" : 10,
                    "firstName" : "カオリ",
                    "friends" : [ ],
                    "lastName" : "大空"
                  } ],
                  "lastName" : "磯野"
                }""");
    }

ここではJsonMapper経由でSerializationFeature#INDENT_OUTPUTを設定しています。

        ObjectMapper mapper = JsonMapper
                .builder()
                .configure(SerializationFeature.INDENT_OUTPUT, true)
                .build();

ObjectMapper#enableはなくなりました。イミュータブルになったからです。

パースに失敗する場合。

    @Test
    void parseFail() {
        ObjectMapper mapper = new ObjectMapper();

        assertThatThrownBy(() -> mapper.readValue("""
                        {
                          "firstName": "カツオ",
                          "lastName": "磯野",
                          "age": 11,
                          "friends":
                        """,
                Person.class)
        )
                .isInstanceOf(JacksonException.class)
                .isExactlyInstanceOf(UnexpectedEndOfInputException.class)
                .hasMessage("""
                        Unexpected end-of-input within/between Object entries
                         at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #UNKNOWN]""");
    }

ここまでの例を見てきてもわかりますが、Jacksonが検査例外を強制しなくなったのでcatch句またはthrowsを都度書かなくても
よくなりました。

Recordsで試す

オマケ的にRecords版も書いておきます。Jackson 2.xと同様、特に問題なく使えます。

src/test/java/org/littlewings/jackson/JacksonRecordTest.java

package org.littlewings.jackson;

import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import tools.jackson.core.JacksonException;
import tools.jackson.core.exc.UnexpectedEndOfInputException;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class JacksonRecordTest {
    @Test
    void deserialize() {
        ObjectMapper mapper = new ObjectMapper();
        PersonRecord katsuo = mapper.readValue("""
                        {
                          "firstName": "カツオ",
                          "lastName": "磯野",
                          "age": 11,
                          "friends": [
                            {
                              "firstName": "弘",
                              "lastName": "中島",
                              "age": 10
                            },
                            {
                              "firstName": "花子",
                              "lastName": "花沢",
                              "age": 11
                            },
                            {
                              "firstName": "カオリ",
                              "lastName": "大空",
                              "age": 10
                            }
                          ]
                        }
                        """,
                PersonRecord.class);

        assertThat(katsuo.firstName()).isEqualTo("カツオ");
        assertThat(katsuo.lastName()).isEqualTo("磯野");
        assertThat(katsuo.age()).isEqualTo(11);

        assertThat(katsuo.friends()).hasSize(3);
        assertThat(katsuo.friends().getFirst().lastName()).isEqualTo("中島");
        assertThat(katsuo.friends().get(1).lastName()).isEqualTo("花沢");
        assertThat(katsuo.friends().getLast().lastName()).isEqualTo("大空");
    }

    @Test
    void serialize() {
        List<PersonRecord> friends = List.of(
                new PersonRecord("弘", "中島", 10, Collections.emptyList()),
                new PersonRecord("花子", "花沢", 11, Collections.emptyList()),
                new PersonRecord("カオリ", "大空", 10, Collections.emptyList())
        );
        PersonRecord katsuo = new PersonRecord("カツオ", "磯野", 11, friends);

        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(katsuo);

        assertThat(json).isEqualTo("""
                {"firstName":"カツオ","lastName":"磯野","age":11,"friends":[{"firstName":"弘","lastName":"中島","age":10,"friends":[]},{"firstName":"花子","lastName":"花沢","age":11,"friends":[]},{"firstName":"カオリ","lastName":"大空","age":10,"friends":[]}]}""");
    }

    @Test
    void prettyPrint() {
        List<PersonRecord> friends = List.of(
                new PersonRecord("弘", "中島", 10, Collections.emptyList()),
                new PersonRecord("花子", "花沢", 11, Collections.emptyList()),
                new PersonRecord("カオリ", "大空", 10, Collections.emptyList())
        );
        PersonRecord katsuo = new PersonRecord("カツオ", "磯野", 11, friends);

        ObjectMapper mapper = JsonMapper
                .builder()
                .configure(SerializationFeature.INDENT_OUTPUT, true)
                .build();
        String json = mapper.writeValueAsString(katsuo);

        assertThat(json).isEqualTo("""
                {
                  "firstName" : "カツオ",
                  "lastName" : "磯野",
                  "age" : 11,
                  "friends" : [ {
                    "firstName" : "弘",
                    "lastName" : "中島",
                    "age" : 10,
                    "friends" : [ ]
                  }, {
                    "firstName" : "花子",
                    "lastName" : "花沢",
                    "age" : 11,
                    "friends" : [ ]
                  }, {
                    "firstName" : "カオリ",
                    "lastName" : "大空",
                    "age" : 10,
                    "friends" : [ ]
                  } ]
                }""");
    }

    @Test
    void parseFail() {
        ObjectMapper mapper = new ObjectMapper();

        assertThatThrownBy(() -> mapper.readValue("""
                        {
                          "firstName": "カツオ",
                          "lastName": "磯野",
                          "age": 11,
                          "friends":
                        """,
                PersonRecord.class)
        )
                .isInstanceOf(JacksonException.class)
                .isExactlyInstanceOf(UnexpectedEndOfInputException.class)
                .hasMessage("""
                        Unexpected end-of-input within/between Object entries
                         at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #UNKNOWN]""");
    }
}

特筆するようなところはないのですが、シリアライズした時にRecordsに定義した順番に並ぶんだな、ということに今回
気づきました(今まであまり気にしていなかった…)。

        assertThat(json).isEqualTo("""
                {
                  "firstName" : "カツオ",
                  "lastName" : "磯野",
                  "age" : 11,
                  "friends" : [ {
                    "firstName" : "弘",
                    "lastName" : "中島",
                    "age" : 10,
                    "friends" : [ ]
                  }, {
                    "firstName" : "花子",
                    "lastName" : "花沢",
                    "age" : 11,
                    "friends" : [ ]
                  }, {
                    "firstName" : "カオリ",
                    "lastName" : "大空",
                    "age" : 10,
                    "friends" : [ ]
                  } ]
                }""");

おわりに

Jackson Databind 3を試してみました。

使い方自体は大きく変わっていないのですんなり入れましたが、2.xから3.xとメジャーバージョンアップしたことでどのような
変化があったのかを押さえておくという意味で見ておいてよかったですね。

これから3.xのJacksonを徐々に見ていくことになるのでしょう。