これは、なにをしたくて書いたもの?
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の両方でアノテーションを使えるようにするため
- jackson-annotationsだけは
- 2.xで非推奨になった機能の削除
- 主要なクラス、メソッド、フィールドの名称変更
- たとえば
JsonStreamContext→TokenStreamContext、JsonLocation→TokenStreamLocation、JsonProcessingException→JacksonException、JsonDeserializer→ValueDeserializerなど
- たとえば
- デフォルト値の変更
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を試してみます。
環境
今回の環境はこちら。
$ 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を徐々に見ていくことになるのでしょう。