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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?