JacksonはJavaでJSONを読み書きするライブラリです。Mavenならpom.xmlにjackson-databindを1つ書けば、残りの部品は自動で入ります。ただし2025年10月に3.0が出て以降、Jacksonは座標もパッケージ名も違う2系と3系が並行して更新されています。どちらを書くかでimport文も既定の挙動も変わります。この記事では、2.22.3と3.2.3の依存の書き方、Java 25で実行して確かめた既定動作の違い、ObjectMapperとJsonNodeを使ったJSONの変換・参照・編集を、実行結果付きで説明します。
まとめ:JacksonをMavenで使うときの要点
- 書く依存は1つ:
jackson-databindを追加すれば、jackson-coreとjackson-annotationsは推移的依存で入る。 - 2系と3系は座標が違う:2系は
com.fasterxml.jackson.core(2026年9月時点の最新2.22.3)、3系はtools.jackson.core(同3.2.3)。3系でもjackson-annotationsだけは2系の座標のまま使う。 - 選び方:Java 17以上の新規開発とSpring Boot 4系は3系。Java 8・11が残る環境やSpring Boot 3系は2系。長く据え置くならLTSの2.21系・3.1系が候補になる。
- 3系は既定動作が変わった:未知のプロパティを無視し、
java.timeを標準で扱い、例外は非検査例外になる。2系のコードをパッケージ名だけ置き換えても同じ結果にはならない。 - JSONの扱い方は2通り:型が決まっているならObjectMapperでrecordやクラスへ変換し、構造が不定ならJsonNodeのツリーで辿る。
以下、依存の書き方、2系と3系の違い、ObjectMapper、JsonNodeの参照と編集、失敗しやすい点の順に説明します。
Jacksonを構成する3つのモジュールと役割
Jacksonは用途ごとにjarが分かれています。普段コードから直接使うのはjackson-databindのObjectMapperとJsonNodeで、残りの2つはその土台です。
| artifactId | 役割 | 主なクラス |
|---|---|---|
| jackson-core | JSONを1トークンずつ読み書きする低水準API | JsonParser、JsonGenerator |
| jackson-annotations | 変換規則を指定するアノテーション | @JsonProperty、@JsonIgnore |
| jackson-databind | JavaオブジェクトやツリーとJSONの変換 | ObjectMapper、JsonNode |
databindはcoreとannotationsに依存しているため、databindだけを書けば3つが揃います。mvn dependency:treeで確かめた結果が次です。annotationsの版がパッチ番号の無い2.22になっているのは、2.20以降annotationsだけが「2.20」「2.21」のようにマイナー版単位で出る方式に変わったためです。
demo:j2:jar:1
\- com.fasterxml.jackson.core:jackson-databind:jar:2.22.3:compile
+- com.fasterxml.jackson.core:jackson-annotations:jar:2.22:compile
\- com.fasterxml.jackson.core:jackson-core:jar:2.22.3:compile
2系の日付・時刻対応(jackson-datatype-jsr310)と、2系・3系のYAML・CSV対応(jackson-dataformat-*)は別モジュールです。3系のjava.time対応はdatabindに組み込まれています。必要になった時点で追加し、版はBOMで揃えます(後述)。
pom.xmlへの追加:2系と3系の依存の書き方
Jackson 2系のMaven座標と対応Java
既存プロジェクトの大半が使っている系統です。jackson-databind 2.22.3はJava 8向けにコンパイルされているので、古いJDKが残る環境でも使えます。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.22.3</version>
</dependency>
パッケージはcom.fasterxml.jackson.databindです。検索でよく見かけるcom.fasterxml.jackson.databind.JsonNodeはこの系統のクラス名です。
Jackson 3系のMaven座標とannotationsの扱い
3系はgroupIdとJavaパッケージがどちらもtools.jacksonに変わりました。3.2.3のpomはJava 17を対象にしています。
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.2.3</version>
</dependency>
このときの依存ツリーでは、annotationsだけが2系の座標で入ります。
demo:j3:jar:1
\- tools.jackson.core:jackson-databind:jar:3.2.3:compile
+- com.fasterxml.jackson.core:jackson-annotations:jar:2.22:compile
\- tools.jackson.core:jackson-core:jar:3.2.3:compile
databind 3.2.3のpomにも「Annotations remain at Jackson 2.x group id」とコメントがあり、@JsonPropertyなどは2系と3系で同じクラスを共有します。注意したいのは、Maven Centralのjackson-annotationsのmaven-metadata.xmlでは、2026年9月27日に取得した時点で<latest>も<release>も3.0-rc5を指していることです。これは正式版ではないRC(リリース候補)なので、「最新」を機械的に拾うツールやスクリプトで版を決めると誤ります。annotationsの版はdatabindの推移的依存かBOMに任せます。アノテーションを直接使う場合は、BOMで版を管理したうえでannotationsを明示的な依存として宣言する方法もあります。
jackson-bomによる関連モジュールのバージョン管理
databindに加えてjackson-datatype-jsr310などを入れる場合は、jackson-bomをdependencyManagementでimportします。各依存から<version>を省き、BOMの管理する組み合わせに揃えられます。ただし、個別の版指定や別のdependencyManagement設定で上書きしていないかは確認が必要です。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.22.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
</dependencies>
3系のBOMはtools.jackson:jackson-bomです。Spring Bootを使っている場合はBootがJacksonの版を管理しているので、BOMも<version>も書かずに済みます。
Gradleで追加する書き方
Gradle(Kotlin DSL)では座標を1行で書きます。BOMはplatform()で読み込みます。
dependencies {
implementation(platform("com.fasterxml.jackson:jackson-bom:2.22.3"))
implementation("com.fasterxml.jackson.core:jackson-databind")
// 3系の場合
// implementation("tools.jackson.core:jackson-databind:3.2.3")
}
Jackson 2.22と3.2の違い:既定動作を実測した対照表
3系への移行はパッケージ名の置換だけでは終わりません。同じJSONと、比較項目に応じたrecord・Beanを2.22.3と3.2.3に渡し、Temurin 25で実行した結果を並べます。
| 項目 | 2.22.3 | 3.2.3 |
|---|---|---|
| groupId/パッケージ | com.fasterxml.jackson | tools.jackson |
| 対象Java(databind) | 8 | 17 |
| 例外の種類 | 検査例外(IOException系) | 非検査例外(RuntimeException系) |
| recordに無い項目を含むJSON | UnrecognizedPropertyException | 無視して変換 |
| intの項目にnull | 0で変換 | MismatchedInputException |
| LocalDateの書き出し | InvalidDefinitionException | “2026-09-27” |
| 出力順(通常のクラス) | 宣言順 | アルファベット順 |
| 出力順(record) | 宣言順 | 宣言順 |
| 入れ子の深さの上限 | 1,000 | 500 |
| 文字列1個の長さの上限 | 20,000,000 | 100,000,000 |
| 設定の変更 | configure()で既存インスタンスを変更 | builder()/rebuild()で新規作成 |
| 文字列値の取得 | asText()/textValue() | asString()/stringValue() |
影響が大きいのは未知項目の扱いです。2系で「APIのレスポンスに想定外の項目が増えたら例外で気づく」設計にしていた場合、3系では黙って通ります。同じ検知を続けたいなら、3系でFAIL_ON_UNKNOWN_PROPERTIESを明示的に有効にします。逆に、intの項目へのnullは2系だと0で素通りし、3系では例外になります。通常のクラスの出力順の変化(recordはどちらも宣言順のまま)は、JSON文字列をそのまま比較しているテストやAPIの署名計算を壊します。3系で宣言順に戻すにはMapperFeature.SORT_PROPERTIES_ALPHABETICALLYを無効にします。3系のasText()は@Deprecated付きで残っているため、コンパイルは通り警告だけが出ます。
どちらを選ぶか:LTSとSpring Bootの管理版
FasterXMLの公式リリース表(Jackson-Releases)では、2.21系と3.1系がLTS(長期サポート)、2.22系と3.2系は最新ブランチ(LTSではない)と位置付けられています。2.21系は少なくとも2028年1月31日まで修正版が出る予定と明記されています。3.0系は2026年2月の3.1.0公開で終了しました。
- 3系を選ぶ:Java 17以上で新規に作る場合、Spring Boot 4系を使う場合。Spring Boot 4.1.1が管理する版は3.1.5で、2系用の
spring-boot-jackson2モジュールは説明文に「deprecated in favor of spring-boot-jackson」と書かれた非推奨扱いです。 - 2系を選ぶ:Java 8・11が残る場合、Spring Boot 3系を使う場合(3.5.16の管理版は2.21.4)、2系前提のライブラリを多く抱える場合。
2系と3系はパッケージが違うため、同じクラスパスに共存させても衝突しません。2.22.3と3.2.3を同時に載せて両方のマッパーでJSONを読めることを確認しました。ライブラリが2系に依存したまま、アプリ側だけ3系へ移る段階的な移行が可能です。Spring Boot 4でのJackson 3対応の全体像はSpring Boot 4の変更点と新機能の解説で扱っています。
ObjectMapperによるJSONとJavaオブジェクトの相互変換
型が決まっているJSONは、ObjectMapperでrecordやクラスへ直接変換するのが最短です。readValueで読み、writeValueAsStringで書きます。リストのように要素型が必要なものはTypeReferenceで型を渡します(以下は2系のコードで、recordを使うためJava 16以上が必要です。Java 8・11では通常のクラスで型を定義してください)。
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
public class BindExample {
// 変換用の型。record でもクラスでもよい
public record User(String name, int age) {}
// ObjectMapper は生成コストが高いので1つを使い回す
private static final ObjectMapper MAPPER = new ObjectMapper();
public static void main(String[] args) throws Exception {
// JSON文字列 → Javaオブジェクト
User user = MAPPER.readValue("{\"name\":\"Sato\",\"age\":31}", User.class);
System.out.println(user);
// Javaオブジェクト → JSON文字列
System.out.println(MAPPER.writeValueAsString(new User("Suzuki", 28)));
// 配列はTypeReferenceで要素の型を渡す
List<User> users = MAPPER.readValue(
"[{\"name\":\"A\",\"age\":1},{\"name\":\"B\",\"age\":2}]",
new TypeReference<List<User>>() {});
System.out.println(users.size() + " " + users.get(1).name());
}
}
User[name=Sato, age=31]
{"name":"Suzuki","age":28}
2 B
recordはアノテーション無しでコンストラクタ経由で変換されます。recordの仕組みはJavaのレコードクラス(record)の解説を参照してください。ObjectMapperをリクエストごとにnewすると、型ごとの変換器のキャッシュが毎回捨てられます。設定を済ませたものをstatic finalで共有するのが定石です。
JsonNodeによるJSONの解析と値の取得
大きなJSONの一部だけを読み、メモリ使用量を抑えたい場合はJsonParserによる逐次処理を検討します。項目が可変で、全体をメモリ上に保持できるJSONは、readTreeでJsonNodeのツリーにして辿ります。クラスを定義しなくて済むのが利点です。
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TreeExample {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
String json = "{\"user\":{\"name\":\"Sato\",\"age\":31,\"tags\":[\"java\",\"maven\"]},\"score\":null}";
JsonNode root = mapper.readTree(json);
System.out.println(root.get("missing")); // null
System.out.println(root.path("missing").isMissingNode()); // true
System.out.println(root.get("score").isNull()); // true(JSONのnull)
JsonNode user = root.path("user");
System.out.println(user.path("name").asText()); // Sato
System.out.println(user.path("age").asInt()); // 31
System.out.println(user.path("age").textValue()); // null(数値ノードなので)
System.out.println(root.path("nope").path("deep").asInt(-1)); // -1(途中が無くても例外にならない)
System.out.println(root.at("/user/tags/1").asText()); // maven
for (JsonNode tag : user.path("tags")) {
System.out.println("tag=" + tag.asText());
}
}
}
get()とpath()の違い
存在しないキーに対して、get()はJavaのnullを返し、path()はMissingNodeを返します。get()を連鎖させると途中でNullPointerExceptionになるので、深い階層を辿るときはpath()を使います。JSON側に"score":nullと書かれている場合は、get()でもnullではなくNullNodeが返り、isNull()がtrueになります。「キーが無い」と「値がnull」を区別したいときは、この2つを使い分けます。
asText()・textValue()・asInt()の戻り値
asText()やasInt()は型を変換して返し、変換できなければ既定値(空文字や0、引数で指定した値)を返します。textValue()は文字列ノードのときだけ値を返し、数値ノードではnullです。3系ではそれぞれasString()とstringValue()に改名され、stringValue()は数値ノードに対してnullではなくJsonNodeExceptionを投げます。型チェックが厳しくなった点は、移行時にテストで拾っておきたい差です。
at()とJSON Pointerによる深い階層の値の取得
at("/user/tags/1")のように、RFC 6901のJSON Pointerで位置を1回で指定できます。配列は0始まりの添字で、存在しない位置ではMissingNodeが返ります。配列ノードはそのまま拡張for文で回せます。
ObjectNode・ArrayNodeによるJSONの編集と文字列化
読み込んだJsonNodeは参照用の型なので、編集するときはObjectNode(オブジェクト)やArrayNode(配列)にキャストします。
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class EditExample {
public record User(String name, int age) {}
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
ObjectNode root = (ObjectNode) mapper.readTree(
"{\"user\":{\"name\":\"Sato\",\"age\":31,\"tags\":[\"java\"]},\"score\":null}");
root.put("active", true); // 追加・上書き
root.remove("score"); // 削除
((ArrayNode) root.path("user").path("tags")).add("maven"); // 配列に追加
root.putObject("meta").put("version", 2); // 子オブジェクトを作って追加
root.withObject("/audit/created").put("by", "batch"); // 途中の階層もまとめて作る
System.out.println(mapper.writeValueAsString(root));
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(root.path("meta")));
// ツリーとJavaオブジェクトの相互変換
ObjectNode userNode = (ObjectNode) root.path("user").deepCopy();
userNode.remove("tags"); // User に無い項目は外す(2系は未知項目で例外)
User user = mapper.treeToValue(userNode, User.class);
JsonNode back = mapper.valueToTree(user);
System.out.println(user + " " + back);
}
}
{"user":{"name":"Sato","age":31,"tags":["java","maven"]},"active":true,"meta":{"version":2},"audit":{"created":{"by":"batch"}}}
{
"version" : 2
}
User[name=Sato, age=31] {"name":"Sato","age":31}
文字列に戻すのはwriteValueAsString、整形するならwriterWithDefaultPrettyPrinter()です。treeToValueはツリーをrecordへ、valueToTreeはオブジェクトをツリーへ変換します。途中でtagsを外しているのは、2系の既定ではUserに無い項目があるとUnrecognizedPropertyExceptionになるためです。外さずに実行すると、実際にこの例外で止まりました。
例外処理と失敗しやすい3つのパターン
検査例外の2系と非検査例外の3系
2系のreadTreeやreadValueはJsonProcessingException(IOExceptionのサブクラス)を宣言しているため、catchかthrowsが必須です。3系ではJacksonExceptionがRuntimeExceptionを継承しており、ラムダやStreamの中でも包まずに呼べます。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ErrorExample {
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
try {
JsonNode node = mapper.readTree("{\"a\":1,}"); // 末尾カンマは不正なJSON
System.out.println(node);
} catch (JsonProcessingException e) { // 2系は検査例外なのでcatchかthrowsが必須
System.out.println(e.getClass().getSimpleName() + ": " + e.getOriginalMessage());
}
}
}
JsonParseException: Unexpected character ('}' (code 125)): was expecting double-quote to start field name
3系で同じ入力を渡すと、例外クラスはtools.jackson.core.exc.StreamReadExceptionになり、メッセージの「field name」も「property name」に変わります。例外メッセージの文字列で分岐しているコードは、移行時に見直しが必要です。
jackson-coreの版ずれで起きるNoClassDefFoundError
databindとcoreの版が食い違うと、コンパイルは通るのに実行時に落ちます。databind 2.22.3に対してcore 2.12.7を直接指定したpomで試すと、Mavenは直接指定した2.12.7を優先し、最初のJSON変換で次の例外が出ました。
Exception in thread "main" java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/exc/StreamConstraintsException
at com.fasterxml.jackson.databind.deser.std.JdkDeserializers.<clinit>(JdkDeserializers.java:31)
実務では、古いcoreを引く別ライブラリが依存ツリーの浅い位置にあるとこの状態になります。mvn dependency:tree -Dincludes=com.fasterxml.jackson.coreで版を確認し、ずれていればjackson-bomで揃えます。
入れ子の深さと文字列長の上限
Jacksonは2.15以降、巨大な入力による資源枯渇を防ぐため読み込みに上限を設けています。既定値をStreamReadConstraints.defaults()で読むと、2.22.3は入れ子1,000段・文字列2,000万文字、3.2.3は入れ子500段・1億文字でした。3.2.3に1,000段の配列を渡すと「Document nesting depth (501) exceeds the maximum allowed (500…)」で失敗します。2系で動いていた深いJSONが3系で読めなくなる場合は、次のように上限を明示します(2系の例)。
JsonFactory factory = JsonFactory.builder()
.streamReadConstraints(StreamReadConstraints.builder().maxNestingDepth(2000).build())
.build();
ObjectMapper deep = new ObjectMapper(factory);
上限を引き上げる場合も、入力の深さと使用可能なメモリに合わせた有限値を設定します。外部から受け取るJSONを任意の型へ変換する設定(デフォルトタイピング)は、上限とは別にリモートコード実行の入口になり得ます。この危険は安全でないデシリアライゼーションの仕組みと防御実装で扱っています。
Jackson 3系で同じ処理を書く場合
3系ではJsonMapper.builder()で設定を決めてから生成し、生成後は変更できません。設定違いが欲しいときはrebuild()で別インスタンスを作ります。2系の挙動に寄せたい設定も、ここで明示します。
import tools.jackson.core.JacksonException;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper;
import java.time.LocalDate;
public class Jackson3Example {
public record Event(String title, LocalDate date) {}
public static void main(String[] args) { // 3系は非検査例外なので throws 不要
JsonMapper mapper = JsonMapper.builder()
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) // 2系と同じく未知項目を例外にする
.build();
// java.time は追加モジュールなしで ISO 形式になる
System.out.println(mapper.writeValueAsString(new Event("release", LocalDate.of(2026, 9, 27))));
JsonNode root = mapper.readTree("{\"name\":\"Sato\",\"age\":31}");
System.out.println(root.path("name").asString()); // 2系の asText() に相当
// 設定を変えるときは rebuild() で別インスタンスを作る
JsonMapper lenient = mapper.rebuild()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
System.out.println(lenient.readValue("{\"title\":\"a\",\"date\":\"2026-01-01\",\"x\":1}", Event.class));
try {
mapper.readValue("{\"title\":\"a\",\"date\":\"2026-01-01\",\"x\":1}", Event.class);
} catch (JacksonException e) {
System.out.println(e.getClass().getSimpleName());
}
}
}
{"title":"release","date":"2026-09-27"}
Sato
Event[title=a, date=2026-01-01]
UnrecognizedPropertyException
2系でLocalDateを扱うにはjackson-datatype-jsr310を追加してJavaTimeModuleを登録します。ただし2系は登録しただけだと[2026,9,27]という配列で書き出し、"2026-09-27"にするにはSerializationFeature.WRITE_DATES_AS_TIMESTAMPSの無効化が別に必要でした。3系はこの2手間が不要です。
よくある質問
jackson-coreやjackson-annotationsも依存に書くべきですか?
通常は書きません。jackson-databindだけで推移的に入ります。版を固定したい事情があるなら、個別に書くよりjackson-bomをimportしてください。個別指定はcoreだけ古い版になる事故の元です。
ObjectMapperはスレッドセーフですか?
2系のJavadocには「最初の読み書きの前に全ての設定を終えていればスレッドセーフ」と書かれています。起動時に設定して共有し、途中でconfigure()を呼ばない限り複数スレッドから使えます。3系のマッパーは生成後に設定を変えられないので、この制約を意識する必要がありません。
Spring Bootでは自分でJacksonを追加する必要がありますか?
spring-boot-starter-webなどのスターターに含まれるので、追加は不要です。Spring Bootは自前のObjectMapper(4系ではJsonMapper)をBeanとして用意し、spring.jackson.*のプロパティで設定を変えられます。Spring Bootの依存の書き方はSpring Bootの4.1系の依存名と最小Web APIの作り方にまとめています。
JsonNodeのtoString()とwriteValueAsString()は同じ結果になりますか?
2.22.3と3.2.3では、エスケープを含め同じ正しいJSON文字列を返しました。整形したい場合は2系のtoPrettyString()かwriterWithDefaultPrettyPrinter()を使います。ObjectMapperに独自の設定(出力時の数値形式など)を入れているときは、その設定を通るwriteValueAsString()を使うほうが確実です。
Mavenでjackson-databindの最新版を確認する方法は?
Maven Centralのmaven-metadata.xmlにある<release>は非SNAPSHOT版を示しますが、RC版などを含む場合があり、正式安定版とは限りません。採用する版はFasterXMLの公式リリース表(Jackson-Releases)でも確認してください。databindの<release>は2026年9月27日時点で2系が2.22.3、3系が3.2.3で、どちらも正式版でした。前述のとおりannotationsは<release>自体がRC版を指しているため、databindの値を見てください。JDKの版を切り替えて試す場合はSDKMAN!によるJVMツールのバージョン管理が便利です。