- JAVA
Jackson ObjectMapperの使い方|JSON変換・日付・record
Jackson とは、Java のオブジェクトと JSON を相互に変換するライブラリで、中心になるのが ObjectMapper の writeValueAsString()(Java → JSON)と readValue()(JSON → Java)です。 Spring Boot の標準の JSON ライブラリでもあり、Java で JSON を扱うときの事実上の定番です。
この記事では、Jackson 2.16.1 と Java 21.0.10 で全コードを実行し、その出力をそのまま掲載しています。基本の変換、List・Map、JsonNode、record、LocalDate・LocalDateTime の日付設定、未知プロパティ・null・snake_case の設定、アノテーション、よく出る例外(UnrecognizedPropertyException・InvalidDefinitionException・MismatchedInputException)の再現と直し方を、やりたいこと別に引ける形にしました。ObjectMapper を使い回したときの速度差(約15倍)の計測や、Jackson 3.x・Spring Boot 4 での書き方の違いも載せています。
Jackson 逆引き早見表(やりたいこと別)
| やりたいこと | 書き方(Jackson 2.x) | 節 |
|---|---|---|
| オブジェクト → JSON | mapper.writeValueAsString(obj) |
基本 |
| JSON → オブジェクト | mapper.readValue(json, Item.class) |
基本 |
| 整形して出力 | mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj) |
基本 |
JSON 配列 → List<T> |
mapper.readValue(json, new TypeReference<List<T>>() {}) |
基本 |
JSON → Map |
mapper.readValue(json, new TypeReference<Map<String, Object>>() {}) |
基本 |
| 一部だけ読む | mapper.readTree(json).path("a").path("b").asText() |
JsonNode |
| JSON Pointer で読む | root.at("/order/items/1/sku") |
JsonNode |
| JSON を組み立てる | mapper.createObjectNode().put("k", v) |
JsonNode |
| record を使う | そのまま(2.12 以降) | 基本 |
LocalDate を扱う |
JavaTimeModule を登録 |
日付 |
日付を "2026-10-02" で出す |
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) |
日付 |
| 日付の書式を指定 | @JsonFormat(pattern = "yyyy/MM/dd") |
日付 |
| 未知のキーを無視 | disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) / @JsonIgnoreProperties(ignoreUnknown = true) |
設定 |
| null を出力しない | serializationInclusion(JsonInclude.Include.NON_NULL) / @JsonInclude |
設定 |
| snake_case にする | propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) |
設定 |
| JSON 側の名前を変える | @JsonProperty("user_id") |
アノテーション |
| 項目を除外 | @JsonIgnore |
アノテーション |
| 読むときに別名も許す | @JsonAlias({"mail", "e_mail"}) |
アノテーション |
| 引数ありコンストラクタで作る | @JsonCreator + @JsonProperty、または record |
例外 |
| ObjectMapper の持ち方 | static final で1つを使い回す(スレッドセーフ) |
性能 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Jackson ObjectMapperの使い方|JSON変換・日付・record」
準備:依存関係と動作確認の環境
Maven
<dependencies>
<!-- databind を入れると core と annotations も入る -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.21.0</version>
</dependency>
<!-- LocalDate / LocalDateTime を使うなら -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.21.0</version>
</dependency>
</dependencies>
Gradle なら implementation("com.fasterxml.jackson.core:jackson-databind:2.21.0") と jackson-datatype-jsr310 を同じ版で追加します。
Spring Boot を使う場合は、spring-boot-starter-web(または spring-boot-starter-json)に Jackson が含まれるので、バージョンの指定は不要です。
| 項目 | 検証で使ったもの | 執筆時点の最新(公式で確認) |
|---|---|---|
| Java | OpenJDK 21.0.10 | Java 21/25(LTS) |
| Jackson 2.x | 2.16.1(core・databind・annotations・datatype-jsr310) | 2.21(LTS)、2.22 |
| Jackson 3.x | (未実行) | 3.1(LTS)、3.2 |
| 実行方法 | java -cp "jackson-*.jar" Basics.java(単一ファイル実行) |
- |
検証環境では Maven Central に接続できなかったため、Gradle に同梱されている Jackson 2.16.1 の jar で実行しました。 この記事で使う 2.x の API は 2.16 から 2.22 まで共通です。Java 25 固有の差分はこの記事の範囲にはありません(Jackson 3.x は Java 17 以上が必要、という点だけ押さえておけば十分です)。
基本:オブジェクト・List・Map・record・JsonNode の変換
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;
import java.util.Map;
public class Basics {
// クラス:引数なしコンストラクタ+getter/setter
public static class Member {
private long id;
private String name;
private List<String> roles;
public Member() {}
public Member(long id, String name, List<String> roles) { this.id = id; this.name = name; this.roles = roles; }
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<String> getRoles() { return roles; }
public void setRoles(List<String> roles) { this.roles = roles; }
@Override public String toString() { return "Member{id=" + id + ", name=" + name + ", roles=" + roles + "}"; }
}
// record(Jackson 2.12 以降はそのまま使える)
public record Task(long id, String title, boolean done) {}
static final ObjectMapper MAPPER = new ObjectMapper();
public static void main(String[] args) throws Exception {
// 1) オブジェクト → JSON
Member m = new Member(1, "佐藤", List.of("admin", "editor"));
String json = MAPPER.writeValueAsString(m);
System.out.println("1) " + json);
// 2) JSON → オブジェクト
Member back = MAPPER.readValue(json, Member.class);
System.out.println("2) " + back);
// 3) 整形して出力
System.out.println("3)\n" + MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(m));
// 4) record ⇔ JSON
String taskJson = MAPPER.writeValueAsString(new Task(10, "棚卸し", false));
System.out.println("4) " + taskJson + " → " + MAPPER.readValue(taskJson, Task.class));
// 5) JSON 配列 → List<Task>(TypeReference で要素の型を伝える)
String arr = "[{\"id\":1,\"title\":\"発注\",\"done\":true},{\"id\":2,\"title\":\"検品\",\"done\":false}]";
List<Task> tasks = MAPPER.readValue(arr, new TypeReference<List<Task>>() {});
System.out.println("5) " + tasks + " 要素の型=" + tasks.get(0).getClass().getSimpleName());
// 5') TypeReference なしだと要素は LinkedHashMap になる
List<?> raw = MAPPER.readValue(arr, List.class);
System.out.println("5') 要素の型=" + raw.get(0).getClass().getSimpleName());
// 6) JSON → Map
Map<String, Object> map = MAPPER.readValue("{\"sku\":\"A-001\",\"qty\":3,\"tags\":[\"new\"]}", new TypeReference<>() {});
System.out.println("6) " + map + " qtyの型=" + map.get("qty").getClass().getSimpleName());
// 7) JsonNode:クラスを作らずに一部だけ読む
JsonNode root = MAPPER.readTree("{\"order\":{\"id\":501,\"items\":[{\"sku\":\"A-001\",\"qty\":2},{\"sku\":\"B-010\",\"qty\":1}]}}");
System.out.println("7) id=" + root.path("order").path("id").asLong()
+ " 2件目sku=" + root.at("/order/items/1/sku").asText()
+ " 無いキー=" + root.path("order").path("memo").asText("(なし)")
+ " get()だと=" + root.get("memo"));
// 8) JsonNode を組み立てる
ObjectNode node = MAPPER.createObjectNode().put("event", "stock.updated").put("qty", 5);
node.putArray("skus").add("A-001").add("B-010");
System.out.println("8) " + MAPPER.writeValueAsString(node));
}
}
実行結果:
1) {"id":1,"name":"佐藤","roles":["admin","editor"]}
2) Member{id=1, name=佐藤, roles=[admin, editor]}
3)
{
"id" : 1,
"name" : "佐藤",
"roles" : [ "admin", "editor" ]
}
4) {"id":10,"title":"棚卸し","done":false} → Task[id=10, title=棚卸し, done=false]
5) [Task[id=1, title=発注, done=true], Task[id=2, title=検品, done=false]] 要素の型=Task
5') 要素の型=LinkedHashMap
6) {sku=A-001, qty=3, tags=[new]} qtyの型=Integer
7) id=501 2件目sku=B-010 無いキー=(なし) get()だと=null
8) {"event":"stock.updated","qty":5,"skus":["A-001","B-010"]}
ポイントを順に見ていきます。
- 1)・2) クラスは「引数なしコンストラクタ+getter/setter」で変換でき、キー名は getter 名(
getName→name)から決まります。 - 4) record は Jackson 2.12 以降、何も付けずに変換できます。 不変で
equalsも自動生成されるので、JSON の受け渡し用のクラスに向いています。 - 5')
TypeReferenceを付けずにList.classで読むと、要素はLinkedHashMapになります。List<Task>として受け取りたいときはnew TypeReference<List<Task>>() {}を渡します(Java の総称型は実行時に消えるため、型を別途伝える必要があります)。 - 6)
Map<String, Object>で読むと、数値はInteger(大きければLong)、配列はArrayList、オブジェクトはLinkedHashMapになります。 - 7)
path()はキーがなくても null を返さず「欠損ノード」を返すので、つないで書いてもNullPointerExceptionになりません。get()はキーがないとnullです。深い場所はat("/order/items/1/sku")(JSON Pointer)で一度に指定できます。
入力はファイル(File)・InputStream・byte[] も渡せ、Map などからクラスへの変換は mapper.convertValue(map, Task.class) です。
日付:LocalDate・LocalDateTime を扱う(JavaTimeModule)
Jackson 2.x は、何も設定しないと java.time(LocalDate など)を変換できません。 jackson-datatype-jsr310 の JavaTimeModule を登録し、WRITE_DATES_AS_TIMESTAMPS を無効にすると、"2026-10-02" のような ISO-8601 形式の文字列になります。
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
public class Dates {
public record Reservation(long id, LocalDate day, LocalDateTime createdAt) {}
public record ReservationFmt(
long id,
@JsonFormat(pattern = "yyyy/MM/dd") LocalDate day,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt) {}
public static void main(String[] args) throws Exception {
var r = new Reservation(1, LocalDate.of(2026, 10, 2), LocalDateTime.of(2026, 10, 2, 9, 30, 15));
// 1) モジュールなし → InvalidDefinitionException
try {
new ObjectMapper().writeValueAsString(r);
} catch (Exception e) {
System.out.println("1) " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse(""));
}
// 2) JavaTimeModule を登録(2.x の既定:日時を数値配列で書く)
ObjectMapper m2 = JsonMapper.builder().addModule(new JavaTimeModule()).build();
System.out.println("2) " + m2.writeValueAsString(r));
// 3) WRITE_DATES_AS_TIMESTAMPS を無効化 → ISO-8601 文字列
ObjectMapper m3 = JsonMapper.builder()
.addModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
String iso = m3.writeValueAsString(r);
System.out.println("3) " + iso);
System.out.println(" 読み戻し: " + m3.readValue(iso, Reservation.class));
// 4) @JsonFormat で書式を指定
var f = new ReservationFmt(2, LocalDate.of(2026, 10, 2), LocalDateTime.of(2026, 10, 2, 9, 30, 15));
String fj = m3.writeValueAsString(f);
System.out.println("4) " + fj + " → " + m3.readValue(fj, ReservationFmt.class));
// 5) OffsetDateTime はオフセット付きで出力される
System.out.println("5) " + m3.writeValueAsString(OffsetDateTime.of(2026, 10, 2, 9, 30, 0, 0, ZoneOffset.ofHours(9))));
// 6) 書式が合わない文字列を読むと
try {
m3.readValue("{\"id\":3,\"day\":\"2026/10/02\",\"createdAt\":\"2026-10-02T09:30:15\"}", Reservation.class);
} catch (Exception e) {
System.out.println("6) " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse(""));
}
}
}
実行結果:
1) InvalidDefinitionException: Java 8 date/time type `java.time.LocalDate` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: Dates$Reservation["day"])
2) {"id":1,"day":[2026,10,2],"createdAt":[2026,10,2,9,30,15]}
3) {"id":1,"day":"2026-10-02","createdAt":"2026-10-02T09:30:15"}
読み戻し: Reservation[id=1, day=2026-10-02, createdAt=2026-10-02T09:30:15]
4) {"id":2,"day":"2026/10/02","createdAt":"2026-10-02 09:30:15"} → ReservationFmt[id=2, day=2026-10-02, createdAt=2026-10-02T09:30:15]
5) "2026-10-02T09:30:00+09:00"
6) InvalidFormatException: Cannot deserialize value of type `java.time.LocalDate` from String "2026/10/02": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '2026/10/02' could not be parsed at index 4
多くの API では ISO-8601 文字列が扱いやすいため、JavaTimeModule の登録と WRITE_DATES_AS_TIMESTAMPS の無効化をセットで設定しておくのがおすすめです。書式の違う文字列を読むと 6) のように InvalidFormatException になるので、相手の書式に合わせて @JsonFormat を付けます。
JsonMapper.builder() は 2.10 から使える書き方で、3.x ではこちらが標準になります。今から書くコードは new ObjectMapper() + configure() より、JsonMapper.builder() で設定を組み立てておくと 3.x への移行が楽になります。Java の日付クラスそのものの使い分けは、別記事「Javaの日付処理」で詳しく扱っています。
設定:未知プロパティ・null 除外・snake_case・アノテーション
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.json.JsonMapper;
public class Options {
public record Product(long id, String name, String note) {}
@JsonIgnoreProperties(ignoreUnknown = true) // このクラスだけ未知キーを無視
public record ProductLoose(long id, String name) {}
public record Account(
@JsonProperty("user_id") long userId, // JSON 側の名前を指定
@JsonAlias({"mail", "e_mail"}) String email, // 読むときだけ別名も受け付ける
@JsonIgnore String passwordHash) {} // 出力にも入力にも使わない
public record Stock(long itemId, int onHand, String warehouseCode) {}
public static void main(String[] args) throws Exception {
String withExtra = "{\"id\":1,\"name\":\"付箋\",\"note\":null,\"color\":\"yellow\"}";
// 1) 未知のプロパティ(color)→ UnrecognizedPropertyException(2.x の既定)
ObjectMapper strict = new ObjectMapper();
try {
strict.readValue(withExtra, Product.class);
} catch (Exception e) {
System.out.println("1) " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse(""));
}
// 2) マッパー全体で無視する / クラス単位で無視する
ObjectMapper lenient = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();
System.out.println("2) " + lenient.readValue(withExtra, Product.class));
System.out.println(" " + strict.readValue(withExtra, ProductLoose.class));
// 3) null のフィールドを出力しない
ObjectMapper nonNull = JsonMapper.builder()
.serializationInclusion(JsonInclude.Include.NON_NULL).build();
Product p = new Product(1, "付箋", null);
System.out.println("3) 既定=" + strict.writeValueAsString(p) + " NON_NULL=" + nonNull.writeValueAsString(p));
// 4) snake_case に一括変換
ObjectMapper snake = JsonMapper.builder()
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE).build();
String sj = snake.writeValueAsString(new Stock(7, 120, "TKY-01"));
System.out.println("4) " + sj + " → " + snake.readValue(sj, Stock.class));
// 5) @JsonProperty / @JsonIgnore / @JsonAlias
Account a = new Account(42, "sato@example.com", "$2a$10$xxxx");
System.out.println("5) " + strict.writeValueAsString(a));
System.out.println(" " + strict.readValue("{\"user_id\":42,\"mail\":\"sato@example.com\"}", Account.class));
}
}
実行結果:
1) UnrecognizedPropertyException: Unrecognized field "color" (class Options$Product), not marked as ignorable (3 known properties: "note", "id", "name"])
2) Product[id=1, name=付箋, note=null]
ProductLoose[id=1, name=付箋]
3) 既定={"id":1,"name":"付箋","note":null} NON_NULL={"id":1,"name":"付箋"}
4) {"item_id":7,"on_hand":120,"warehouse_code":"TKY-01"} → Stock[itemId=7, onHand=120, warehouseCode=TKY-01]
5) {"email":"sato@example.com","user_id":42}
Account[userId=42, email=sato@example.com, passwordHash=null]
よく使う設定の一覧
| やりたいこと | マッパー全体(JsonMapper.builder) | クラス・フィールド単位(アノテーション) |
|---|---|---|
| 未知のキーを無視 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) |
@JsonIgnoreProperties(ignoreUnknown = true) |
| null を出さない | .serializationInclusion(JsonInclude.Include.NON_NULL) |
@JsonInclude(JsonInclude.Include.NON_NULL) |
| snake_case | .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) |
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) |
| 名前を個別に変える | - | @JsonProperty("user_id") |
| 読むときの別名 | - | @JsonAlias({"mail"}) |
| 出力・入力から除外 | - | @JsonIgnore |
| 空のクラスでも書き出す | .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) |
- |
読み取りのポイント
- 1) 2.x の既定では、クラスにない
colorがあるとUnrecognizedPropertyExceptionになります。 項目が増えることがある外部 API の応答は無視の設定に、自分たちで定義した入力は既定のまま(エラー)にしておくと入力ミスに気づけます。 - 5)
@JsonIgnoreを付けたpasswordHashは出力されません。 パスワードのハッシュや内部用の値を API の応答に含めないために使います。 - 5) record に
@JsonPropertyを付けると、出力の順番が変わることがあります(今回はemailが先)。順番を固定したいときは@JsonPropertyOrderを付けます。
よく出る例外の再現と直し方
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
public class Errors {
// 引数ありコンストラクタだけのクラス(引数なしコンストラクタがない)
public static class Item {
private final String name;
public Item(String name) { this.name = name; }
public String getName() { return name; }
}
// getter も public フィールドもないクラス
public static class Empty {
private String secret = "x";
}
public record Qty(String sku, int qty) {}
interface Action { Object run() throws Exception; }
static void t(String label, Action a) {
try {
System.out.println(label + " → 成功: " + a.run());
} catch (Exception e) {
System.out.println(label + "\n " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse(""));
}
}
public static void main(String[] args) {
ObjectMapper m = new ObjectMapper();
t("A) 引数なしコンストラクタがないクラスへ読み込み", () -> m.readValue("{\"name\":\"ペン\"}", Item.class));
t("B) プロパティが1つもないクラスを書き出し", () -> m.writeValueAsString(new Empty()));
t("C) 配列を1件のオブジェクトとして読む", () -> m.readValue("[{\"sku\":\"A\",\"qty\":1}]", Qty.class));
t("D) オブジェクトを List として読む", () -> m.readValue("{\"sku\":\"A\",\"qty\":1}", List.class));
t("E) 数値の項目に文字列", () -> m.readValue("{\"sku\":\"A\",\"qty\":\"three\"}", Qty.class));
t("F) 空文字列を読む", () -> m.readValue("", Qty.class));
t("G) 壊れたJSON(カンマ抜け)", () -> m.readValue("{\"sku\":\"A\" \"qty\":1}", Qty.class));
t("H) 数値の項目に \"3\"(文字列の数字)", () -> m.readValue("{\"sku\":\"A\",\"qty\":\"3\"}", Qty.class));
t("I) int の項目が null", () -> m.readValue("{\"sku\":\"A\",\"qty\":null}", Qty.class));
}
}
実行結果:
A) 引数なしコンストラクタがないクラスへ読み込み
MismatchedInputException: Cannot construct instance of `Errors$Item` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
B) プロパティが1つもないクラスを書き出し
InvalidDefinitionException: No serializer found for class Errors$Empty and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
C) 配列を1件のオブジェクトとして読む
MismatchedInputException: Cannot deserialize value of type `Errors$Qty` from Array value (token `JsonToken.START_ARRAY`)
D) オブジェクトを List として読む
MismatchedInputException: Cannot deserialize value of type `java.util.ArrayList<java.lang.Object>` from Object value (token `JsonToken.START_OBJECT`)
E) 数値の項目に文字列
InvalidFormatException: Cannot deserialize value of type `int` from String "three": not a valid `int` value
F) 空文字列を読む
MismatchedInputException: No content to map due to end-of-input
G) 壊れたJSON(カンマ抜け)
JsonParseException: Unexpected character ('"' (code 34)): was expecting comma to separate Object entries
H) 数値の項目に "3"(文字列の数字) → 成功: Qty[sku=A, qty=3]
I) int の項目が null → 成功: Qty[sku=A, qty=0]
InvalidDefinitionException:引数なしコンストラクタがないクラス
引数2つのコンストラクタしかないクラスを読み込んだときのエラーと、2つの直し方です。
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Ctor {
// 引数2つのコンストラクタだけ
public static class Item {
private final String name;
private final int price;
public Item(String name, int price) { this.name = name; this.price = price; }
public String getName() { return name; }
public int getPrice() { return price; }
@Override public String toString() { return "Item{" + name + ", " + price + "}"; }
}
// 直し方1:@JsonCreator + @JsonProperty で引数と JSON のキーを対応付ける
public static class ItemFixed {
private final String name;
private final int price;
@JsonCreator
public ItemFixed(@JsonProperty("name") String name, @JsonProperty("price") int price) { this.name = name; this.price = price; }
public String getName() { return name; }
public int getPrice() { return price; }
@Override public String toString() { return "ItemFixed{" + name + ", " + price + "}"; }
}
// 直し方2:record にする
public record ItemRecord(String name, int price) {}
public static void main(String[] args) throws Exception {
ObjectMapper m = new ObjectMapper();
String json = "{\"name\":\"ペン\",\"price\":120}";
try {
m.readValue(json, Item.class);
} catch (Exception e) {
System.out.println("直す前: " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse(""));
}
System.out.println("直し方1: " + m.readValue(json, ItemFixed.class));
System.out.println("直し方2: " + m.readValue(json, ItemRecord.class));
}
}
実行結果:
直す前: InvalidDefinitionException: Cannot construct instance of `Ctor$Item` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
直し方1: ItemFixed{ペン, 120}
直し方2: ItemRecord[name=ペン, price=120]
Jackson は「引数なしコンストラクタで作ってから setter で値を入れる」のが既定の作り方です。 それができないクラスでは、@JsonCreator と @JsonProperty でコンストラクタの引数と JSON のキーを対応付けるか、record にするのがいちばん簡単です。なお、上の Errors.java の A) のように引数1つのコンストラクタだけのクラスでは、例外が MismatchedInputException になりました(引数1つのコンストラクタは「値そのものから作る」用途にも解釈されるため)。直し方は同じです。
例外の早見表
| 例外 | 典型的なメッセージ | 主な原因 | 直し方 |
|---|---|---|---|
UnrecognizedPropertyException |
Unrecognized field "color" ... not marked as ignorable |
クラスにないキーがある | FAIL_ON_UNKNOWN_PROPERTIES 無効、@JsonIgnoreProperties(ignoreUnknown = true)、またはフィールド追加 |
InvalidDefinitionException |
Java 8 date/time type ... not supported by default |
JavaTimeModule 未登録 |
addModule(new JavaTimeModule()) |
InvalidDefinitionException |
no Creators, like default constructor, exist |
引数なしコンストラクタがない | 引数なしコンストラクタ追加、@JsonCreator、record |
InvalidDefinitionException |
No serializer found for class ... |
getter も public フィールドもない | getter を追加、@JsonProperty を付ける |
MismatchedInputException |
from Array value (token JsonToken.START_ARRAY) |
配列を1件として読んだ | TypeReference<List<T>> で読む |
MismatchedInputException |
from Object value (token JsonToken.START_OBJECT) |
オブジェクトを List で読んだ | 1件の型で読む |
MismatchedInputException |
No content to map due to end-of-input |
空文字列・空のレスポンス | 読む前に空かどうかを確認 |
InvalidFormatException |
Cannot deserialize value of type int from String "three" |
型が合わない値 | 入力を直す、型を String にして検証 |
JsonParseException |
was expecting comma to separate Object entries |
JSON の構文が壊れている | 送信側の JSON を確認 |
例外の親子関係を知っておくと、catch を整理できます。 UnrecognizedPropertyException と InvalidFormatException は MismatchedInputException の子で、さらにその上は JsonMappingException → JsonProcessingException(2.x はチェック例外)です。
「エラーにならない」ことにも注意
H) と I) の結果のとおり、Jackson 2.x は "3"(文字列の数字)を int の 3 に、null を int の 0 に変換します。 入力を厳密に検証したいときは、MapperFeature.ALLOW_COERCION_OF_SCALARS を無効にする、DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES を有効にする、または Integer で受けて Bean Validation で検証する、といった方法があります。前の2つを設定して同じ入力を読んだ結果です。
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.json.JsonMapper;
public class Strict {
public record Qty(String sku, int qty) {}
public static void main(String[] a) {
ObjectMapper m = JsonMapper.builder().disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES).build();
for (String j : new String[]{"{\"sku\":\"A\",\"qty\":\"3\"}", "{\"sku\":\"A\",\"qty\":null}"}) {
try { System.out.println(m.readValue(j, Qty.class)); }
catch (Exception e) { System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse("")); }
}
}
}
MismatchedInputException: Cannot coerce String value ("3") to `int` value (but might if coercion using `CoercionConfig` was enabled)
MismatchedInputException: Cannot map `null` into type `int` (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)
どちらも MismatchedInputException になり、あいまいな入力を受け付けなくなりました。
ObjectMapper は使い回す(速度とスレッドセーフを計測)
ObjectMapper の生成は重い処理で、内部にクラスごとのシリアライザーのキャッシュを持ちます。 毎回 new するとキャッシュが効かず、変換のたびに準備がやり直しになります。設定を済ませた ObjectMapper はスレッドセーフなので、1つを共有して使えます。
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.ArrayList;
import java.util.List;
public class ReuseBench {
public record Task(long id, String title, boolean done) {}
static final ObjectMapper SHARED = new ObjectMapper();
static long eachTimeNew(int n) throws Exception {
long t0 = System.nanoTime();
for (int i = 0; i < n; i++) {
ObjectMapper m = new ObjectMapper(); // 毎回作る
String s = m.writeValueAsString(new Task(i, "task-" + i, i % 2 == 0));
m.readValue(s, Task.class);
}
return (System.nanoTime() - t0) / 1_000_000;
}
static long shared(int n) throws Exception {
long t0 = System.nanoTime();
for (int i = 0; i < n; i++) {
String s = SHARED.writeValueAsString(new Task(i, "task-" + i, i % 2 == 0)); // 使い回す
SHARED.readValue(s, Task.class);
}
return (System.nanoTime() - t0) / 1_000_000;
}
public static void main(String[] args) throws Exception {
int n = 20_000;
eachTimeNew(2_000); shared(2_000); // ウォームアップ
for (int r = 1; r <= 3; r++) {
System.out.printf("round %d: 毎回new=%dms 使い回し=%dms%n", r, eachTimeNew(n), shared(n));
}
// 1つの ObjectMapper を 8 スレッドから同時に使う
ExecutorService pool = Executors.newFixedThreadPool(8);
AtomicInteger mismatch = new AtomicInteger();
List<Future<?>> fs = new ArrayList<>();
long t0 = System.nanoTime();
for (int th = 0; th < 8; th++) {
final int base = th * 1_000_000;
fs.add(pool.submit(() -> {
for (int i = 0; i < 50_000; i++) {
Task in = new Task(base + i, "t" + (base + i), i % 3 == 0);
try {
Task out = SHARED.readValue(SHARED.writeValueAsString(in), Task.class);
if (!in.equals(out)) mismatch.incrementAndGet();
} catch (Exception e) { mismatch.incrementAndGet(); }
}
}));
}
for (Future<?> f : fs) f.get();
pool.shutdown();
System.out.printf("8スレッド×5万件 共有マッパー: 不一致=%d件 (%dms)%n", mismatch.get(), (System.nanoTime() - t0) / 1_000_000);
}
}
実行結果(2万回の書き出し+読み込み):
round 1: 毎回new=1469ms 使い回し=95ms
round 2: 毎回new=1070ms 使い回し=65ms
round 3: 毎回new=758ms 使い回し=56ms
8スレッド×5万件 共有マッパー: 不一致=0件 (1349ms)
毎回 new すると約0.8〜1.5秒、使い回すと約0.06〜0.1秒で、約13〜17倍の差になりました。8スレッドから同じマッパーを同時に使った40万件の変換でも、結果の不一致は0件でした。
用途ごとに少しだけ設定を変えたいときは、mapper.writerWithDefaultPrettyPrinter() や mapper.readerFor(Task.class) で ObjectWriter/ObjectReader を作ります。 これらも不変でスレッドセーフです。
Spring Boot での設定(application.properties)
Spring Boot は Jackson のマッパーを自動で1つ作り、@RestController の JSON 変換に使います。 設定は application.properties(または application.yml)の spring.jackson.* で変えられます。ここは検証環境で Spring Boot を取得できなかったため未実行で、公式リファレンスに沿って記載しています。
Spring Boot 3.x(Jackson 2.x)
# 整形出力
spring.jackson.serialization.indent-output=true
# null を出力しない
spring.jackson.default-property-inclusion=non_null
# snake_case
spring.jackson.property-naming-strategy=SNAKE_CASE
# 未知のキーでエラーにする(Boot の既定は無視)
spring.jackson.deserialization.fail-on-unknown-properties=true
# タイムゾーン
spring.jackson.time-zone=Asia/Tokyo
Spring Boot 3.x は既定で WRITE_DATES_AS_TIMESTAMPS と FAIL_ON_UNKNOWN_PROPERTIES を無効にしており、JavaTimeModule も自動で登録されます。 そのため、素の Jackson で起きる「日付が数値配列になる」「未知のキーでエラー」は、Spring Boot の中では起きにくくなっています。
Spring Boot 4.x(Jackson 3.x)
Spring Boot 4 では Jackson 3 が既定になり、自動設定されるのは JsonMapper(tools.jackson.databind.json.JsonMapper)です(Spring Boot 4.1 の公式リファレンスで確認)。spring.jackson.* のプロパティは引き続き使え、日付の設定は DateTimeFeature に対応する次の形になりました。
spring.jackson.serialization.indent-output=true
spring.jackson.default-property-inclusion=non_null
spring.jackson.datatype.datetime.write-dates-as-timestamps=false
# Jackson 2 と同じ既定値に寄せる(移行時の互換用)
spring.jackson.use-jackson2-defaults=true
コードでまとめて設定したいときは、Boot 3.x では Jackson2ObjectMapperBuilderCustomizer、Boot 4.x では JsonMapperBuilderCustomizer の Bean を定義します。
Jackson 2.x と 3.x の違い
Jackson 3.0 は 2025年10月に正式版が出た新しいメジャーバージョンです(執筆時点の LTS は 3.1)。公式の移行ガイドで確認した主な違いは次のとおりです。
| 項目 | Jackson 2.x | Jackson 3.x |
|---|---|---|
| パッケージ・groupId | com.fasterxml.jackson |
tools.jackson(アノテーションだけは com.fasterxml.jackson.annotation のまま) |
| 必要な Java | Java 8 以上 | Java 17 以上 |
| マッパーの作り方 | new ObjectMapper() + configure() も可 |
不変。JsonMapper.builder()...build() で作る |
| 例外 | JsonProcessingException(チェック例外) |
JacksonException(非チェック例外)。throws 不要 |
java.time 対応 |
jackson-datatype-jsr310 を追加して登録 |
databind に組み込み |
WRITE_DATES_AS_TIMESTAMPS の既定 |
有効(数値配列) | 無効(ISO-8601 文字列) |
FAIL_ON_UNKNOWN_PROPERTIES の既定 |
有効(エラー) | 無効(無視) |
| プロパティの並び順 | 宣言順が基本 | アルファベット順が既定 |
| 末尾の余分なトークン | 既定では見ない | 既定でエラー |
Jackson 3.x の書き方の例です(検証環境では 3.x の jar を取得できなかったため未実行、移行ガイドに沿った記述です)。
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.SerializationFeature;
JsonMapper mapper = JsonMapper.builder()
.enable(SerializationFeature.INDENT_OUTPUT)
.build(); // できたマッパーは不変
String json = mapper.writeValueAsString(task); // 例外は JacksonException(非チェック)
Task back = mapper.readValue(json, Task.class);
2.x のコードでも JsonMapper.builder() で設定を組み立て、日付は ISO 文字列、未知のキーは用途に合わせて明示的に設定しておくと、3.x に移ったときの差が小さくなります。
よくある質問
Q. Gson と Jackson はどちらを使えばよいですか? A. Spring Boot を使うなら標準の Jackson が自然です。Jackson は日付・record・アノテーション・ストリーミング処理など機能が豊富で、Gson は設定が少なく手軽です。この記事の内容は Jackson を前提にしています。
Q. フィールドが private で getter がないと、どうなりますか?
A. 書き出しでは、そのフィールドは出力されません(全部がそうなら Errors.java の B) のように InvalidDefinitionException)。getter を付けるか、フィールドに @JsonProperty を付けます。
まとめ
writeValueAsString()で Java → JSON、readValue()で JSON → Java。record も 2.12 以降はそのまま使えるList<T>・MapはTypeReference、一部だけ読むならreadTree()+path()/at()- 日付は
JavaTimeModule+WRITE_DATES_AS_TIMESTAMPS無効で"2026-10-02"形式になる - 未知のキー・null・snake_case は
JsonMapper.builder()かアノテーションで設定 - 例外は型で原因がわかる。コンストラクタがないクラスは
@JsonCreatorか record で直す - ObjectMapper は1つを使い回す。毎回
newすると約13〜17倍遅く、共有しても不一致は0件だった - Jackson 3.x は
tools.jackson・不変のJsonMapper・非チェック例外。Spring Boot 4 は 3.x が既定
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Jackson ObjectMapperの使い方|JSON変換・日付・record」
株式会社RJCでは、JacksonによるJSON処理の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。