Gsonとは、JavaのオブジェクトとJSONを相互に変換するGoogle製のライブラリで、toJson でJSONへ、fromJson でJavaへ変換します。 jarが1つで済み、クラスにアノテーションを付けなくてもフィールド名のまま変換できる手軽さが持ち味です。

この記事では、基本の2メソッドからList変換、record・LocalDateTime・Optionalの扱いまでを、すべて動かした結果つきで紹介します。例外メッセージと直し方の対応表、GsonBuilderの設定一覧、Jacksonとの比較表も用意したので、調べものの入口として使ってください。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10 / Gson 2.13.1(比較用のJacksonは 2.16.1)。コードは java -cp gson-2.13.1.jar Basic.java の形で単一ファイルのまま実行しています。

Gsonの早見表

やりたいこと 書き方 ポイント
Java → JSON gson.toJson(obj) nullのフィールドは出力されない
JSON → Java gson.fromJson(json, Member.class) JSONに無い項目はnull/0/false
JSON配列 → List gson.fromJson(json, new TypeToken<List<Item>>() {}) Listには必ずTypeToken
JSON配列 → 配列 gson.fromJson(json, Item[].class) TypeToken不要
JSON → Map new TypeToken<Map<String, Item>>() {} 値の型もTypeTokenで渡す
キー名を変える @SerializedName("article_id") alternate で別名も読める
出力から外す transient または @Expose @Expose はGsonBuilderの設定とセット
整形して出力 new GsonBuilder().setPrettyPrinting().create() 設定はGsonBuilderに集める
nullも出力 new GsonBuilder().serializeNulls().create() 既定は省略
日時型を扱う registerTypeAdapter(LocalDateTime.class, ...) java.timeはアダプタ登録が必要
クラスを作らずに読む JsonParser.parseString(json).getAsJsonObject() get("id").getAsInt() で値を取る

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Gsonの使い方|toJson・fromJson・List変換」

Gsonを使えるようにする(Maven・Gradle)

GsonはMaven Centralで配布されています。公式READMEに載っている依存の書き方は次のとおりです(2026-10-04時点のREADMEの記載は 2.14.0)。

<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.14.0</version>
</dependency>
dependencies {
  implementation 'com.google.code.gson:gson:2.14.0'
}

同じREADMEによると、Gson 2.12.0以降はJava 8以上で動きます。また「現在はメンテナンスモードで、既存の不具合は直すが、大きな新機能は追加されにくい」とも書かれています。APIが落ち着いているぶん、覚えたことが長く使えるライブラリです。

toJsonとfromJsonの基本

まずは最小の例です。Gson のインスタンスを作り、toJson と fromJson を呼ぶだけです。

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Basic {
    static class Member {
        int id;
        String name;
        boolean active;
        String memo;          // null のまま
        Member(int id, String name, boolean active) {
            this.id = id; this.name = name; this.active = active;
        }
    }

    public static void main(String[] args) {
        Gson gson = new Gson();

        // Java → JSON
        Member m = new Member(1, "佐藤", true);
        String json = gson.toJson(m);
        System.out.println(json);

        // JSON → Java
        Member back = gson.fromJson("{\"id\":2,\"name\":\"鈴木\",\"active\":false}", Member.class);
        System.out.println(back.id + " / " + back.name + " / " + back.active + " / " + back.memo);

        // 整形して出力
        Gson pretty = new GsonBuilder().setPrettyPrinting().create();
        System.out.println(pretty.toJson(m));
    }
}

実行結果:

{"id":1,"name":"佐藤","active":true}
2 / 鈴木 / false / null
{
  "id": 1,
  "name": "佐藤",
  "active": true
}

この結果から、Gsonの既定の動きが3つ読み取れます。

  • nullのフィールド(memo)はJSONに出力されない
  • JSONに無い項目(memo)は、読み込み後もnullのまま(例外にならない)
  • 引数なしコンストラクタが無いクラスでも、インスタンスを作って値を入れてくれる

3つ目には注意点が1つあり、後半の「フィールドの初期値が消えるとき」で扱います。

List・配列・MapをJSONと変換する

検索でいちばん多い疑問が「JSON配列をListで受けたい」です。答えは TypeTokenで要素の型まで伝える ことです。

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

public class ListDemo {
    static class Item {
        String name;
        int price;
    }

    public static void main(String[] args) {
        Gson gson = new Gson();
        String json = "[{\"name\":\"ノート\",\"price\":180},{\"name\":\"ペン\",\"price\":120}]";

        // 1. TypeToken で List<Item> を指定する
        List<Item> items = gson.fromJson(json, new TypeToken<List<Item>>() {});
        System.out.println("1: " + items.get(0).name + " " + items.get(0).price);

        // 2. Type を変数に取る書き方(従来からある形)
        Type type = new TypeToken<List<Item>>() {}.getType();
        List<Item> items2 = gson.fromJson(json, type);
        System.out.println("2: " + items2.size() + "件");

        // 3. 要素の型を実行時に決める
        Type t3 = TypeToken.getParameterized(List.class, Item.class).getType();
        List<Item> items3 = gson.fromJson(json, t3);
        System.out.println("3: " + items3.get(1).name);

        // 4. 配列で受ける(TypeToken 不要)
        Item[] array = gson.fromJson(json, Item[].class);
        System.out.println("4: " + array.length + "件 → List.of(array) で List にできる");

        // 5. Map<String, Item>
        String mapJson = "{\"a\":{\"name\":\"ノート\",\"price\":180}}";
        Map<String, Item> map = gson.fromJson(mapJson, new TypeToken<Map<String, Item>>() {});
        System.out.println("5: " + map.get("a").name);

        // 6. List → JSON はそのまま渡せる
        System.out.println("6: " + gson.toJson(items));
    }
}

実行結果:

1: ノート 180
2: 2件
3: ペン
4: 2件 → List.of(array) で List にできる
5: ノート
6: [{"name":"ノート","price":180},{"name":"ペン","price":120}]

new TypeToken<...>() {} の末尾の {} は、無名クラスを作るための記号です。Javaのジェネリクスは実行時に型引数が消えますが、無名クラスにすると型引数の情報がクラスに残るため、Gsonがそこから Item を読み取れます。JavaからJSONへ出すとき(6番)は、中身のオブジェクトから型がわかるのでTypeTokenは要りません。

TypeTokenなしでListを読むとClassCastExceptionになる

List.class を渡しても、コンパイルは通り、読み込みも例外なく終わります。止まるのは要素を取り出す行です。

import com.google.gson.Gson;
import java.util.List;

public class NoToken {
    static class Item {
        String name;
        int price;
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Gson gson = new Gson();
        String json = "[{\"name\":\"ノート\",\"price\":180},{\"name\":\"ペン\",\"price\":120}]";

        List<Item> items = gson.fromJson(json, List.class);   // 要素の型が伝わらない
        System.out.println("size = " + items.size());          // ここまでは動く
        Object first = ((List<?>) items).get(0);
        System.out.println(first.getClass().getName());
        System.out.println(first);

        Item item = items.get(0);                               // ここで例外
        System.out.println(item.name);
    }
}

実行結果:

size = 2
com.google.gson.internal.LinkedTreeMap
{name=ノート, price=180.0}
Exception in thread "main" java.lang.ClassCastException: class com.google.gson.internal.LinkedTreeMap cannot be cast to class NoToken$Item (com.google.gson.internal.LinkedTreeMap is in unnamed module of loader 'app'; NoToken$Item is in unnamed module of loader com.sun.tools.javac.launcher.Main$MemoryClassLoader @1e802ef9)
    at NoToken.main(NoToken.java:21)

List.class には「要素は Item」という情報がありません。そのためGsonは、JSONオブジェクトを LinkedTreeMap(Gson内部のMap)、数値を Double として組み立てます。price が 180.0 になっているのがその印です。size() までは動くので、原因の行と例外の行が離れて見えるのがこのエラーの特徴です。

直し方は、List.class を new TypeToken<List<Item>>() {} に変えるだけです。公式のTroubleshootingにも、fromJson(..., List.class) のような型引数なしの呼び出しを避けてTypeTokenを使う、という解決策が載っています。

キー名を変える・出力する項目を選ぶ

JSON側が article_id のようなスネークケースのときや、出したくない項目があるときの書き方です。

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class Names {
    static class Article {
        @SerializedName("article_id")
        int id = 10;

        @SerializedName(value = "title", alternate = {"subject", "headline"})
        String title = "Gson入門";

        String authorName = "佐藤";
        transient String cache = "出力しない";
        String note;   // null
    }

    static class Account {
        @Expose String loginId = "sato";
        @Expose(serialize = false) String password = "secret";
        String internalMemo = "社内メモ";
    }

    public static void main(String[] args) {
        Gson gson = new Gson();
        System.out.println("既定        : " + gson.toJson(new Article()));

        Gson snake = new GsonBuilder()
                .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
                .create();
        System.out.println("スネーク    : " + snake.toJson(new Article()));

        Gson nulls = new GsonBuilder().serializeNulls().create();
        System.out.println("null も出力 : " + nulls.toJson(new Article()));

        Article a = gson.fromJson("{\"article_id\":7,\"headline\":\"別名で読めた\"}", Article.class);
        System.out.println("alternate   : " + a.id + " / " + a.title);

        Gson expose = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
        System.out.println("@Expose     : " + expose.toJson(new Account()));
        Account acc = expose.fromJson("{\"loginId\":\"suzuki\",\"password\":\"pw\"}", Account.class);
        System.out.println("読み込み    : " + acc.loginId + " / " + acc.password);
    }
}

実行結果:

既定        : {"article_id":10,"title":"Gson入門","authorName":"佐藤"}
スネーク    : {"article_id":10,"title":"Gson入門","author_name":"佐藤"}
null も出力 : {"article_id":10,"title":"Gson入門","authorName":"佐藤","note":null}
alternate   : 7 / 別名で読めた
@Expose     : {"loginId":"sato"}
読み込み    : suzuki / pw
やりたいこと 使うもの 確認できた動き
1項目だけキー名を変える @SerializedName("article_id") 出力も読み込みもその名前になる
読み込み時だけ別名も受ける alternate = {"subject", "headline"} headline でも title に入った
全項目をスネークケースに setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES) authorName が author_name に。@SerializedName が優先
常に対象から外す transient cache は出力されない
指定した項目だけ対象にする @Expose と excludeFieldsWithoutExposeAnnotation() 印の無い internalMemo は出ない
読むが書かない @Expose(serialize = false) password は出力されず、読み込みはできた

recordをGsonで変換する

recordも、Gson 2.13.1ではそのまま変換できます。

import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.util.List;

public class RecordDemo {
    record Tag(String label) {}

    record Task(int id, String title, boolean done,
                @SerializedName("due_date") String dueDate,
                List<Tag> tags) {}

    record Stock(String code, int quantity) {
        Stock {
            if (quantity < 0) throw new IllegalArgumentException("quantity は 0 以上: " + quantity);
        }
    }

    public static void main(String[] args) {
        Gson gson = new Gson();

        Task task = new Task(1, "見積書を送る", false, "2026-10-09", List.of(new Tag("営業")));
        String json = gson.toJson(task);
        System.out.println("1: " + json);

        Task back = gson.fromJson(json, Task.class);
        System.out.println("2: " + back);
        System.out.println("3: equals = " + task.equals(back));

        // JSON に無い項目は null、プリミティブは 0 / false
        Task partial = gson.fromJson("{\"title\":\"項目が足りないJSON\"}", Task.class);
        System.out.println("4: " + partial);

        // コンパクトコンストラクタの検査も通る
        try {
            gson.fromJson("{\"code\":\"A-1\",\"quantity\":-5}", Stock.class);
        } catch (Exception e) {
            System.out.println("5: " + e.getClass().getName());
            System.out.println("   " + e.getMessage());
            System.out.println("   cause: " + e.getCause());
        }
    }
}

実行結果:

1: {"id":1,"title":"見積書を送る","done":false,"due_date":"2026-10-09","tags":[{"label":"営業"}]}
2: Task[id=1, title=見積書を送る, done=false, dueDate=2026-10-09, tags=[Tag[label=営業]]]
3: equals = true
4: Task[id=0, title=項目が足りないJSON, done=false, dueDate=null, tags=null]
5: java.lang.RuntimeException
   Failed to invoke constructor 'RecordDemo$Stock(String, int)' with args [A-1, -5]
   cause: java.lang.IllegalArgumentException: quantity は 0 以上: -5
  • recordのコンポーネントに @SerializedName を付けられる(due_date)
  • 読み戻したrecordは元と equals で一致する
  • JSONに無い項目は、参照型がnull、プリミティブが0/false でコンストラクタに渡される
  • コンパクトコンストラクタの検査は読み込み時にも働く。検査で例外を投げると、Gsonは RuntimeException(Failed to invoke constructor …)に包んで投げ、元の例外は getCause() に入る

recordは必ずコンストラクタを通るため、「不正な値のオブジェクトが黙ってできあがる」ことを防げます。値の検査を入れたいデータにはrecordが向いています。

LocalDateTimeをGsonで変換する

java.time の型は、アダプタを登録せずに渡すと例外になります。まずはそのままの例です。

import com.google.gson.Gson;
import java.time.LocalDateTime;

public class TimeNg {
    record Order(int id, LocalDateTime orderedAt) {}

    public static void main(String[] args) {
        Gson gson = new Gson();
        Order order = new Order(1, LocalDateTime.of(2026, 10, 4, 9, 30, 0));
        try {
            System.out.println(gson.toJson(order));
        } catch (Exception e) {
            System.out.println("toJson  : " + e.getClass().getName());
            System.out.println(e.getMessage());
        }
        try {
            System.out.println(gson.fromJson("{\"id\":1,\"orderedAt\":\"2026-10-04T09:30:00\"}", Order.class));
        } catch (Exception e) {
            System.out.println("fromJson: " + e.getClass().getName());
            System.out.println(e.getMessage());
        }
    }
}

実行結果:

toJson  : com.google.gson.JsonIOException
Failed making field 'java.time.LocalDateTime#date' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type.
See https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible
fromJson: com.google.gson.JsonIOException
Failed making field 'java.time.LocalDateTime#date' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type.
See https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible

Gsonは専用の変換方法を知らない型を、フィールドを1つずつ読むリフレクションで変換しようとします。ところが LocalDateTime の内部フィールド(date)はJDKのモジュールの内側にあり、外から触れません。そこで JsonIOException になります。メッセージの「write a custom TypeAdapter」が、そのまま直し方です。

TypeAdapterを登録して直す

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class TimeOk {
    record Order(int id, LocalDateTime orderedAt, LocalDate shipDate) {}

    // LocalDateTime ⇔ "2026-10-04T09:30:00"
    static class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {
        private static final DateTimeFormatter F = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

        @Override
        public void write(JsonWriter out, LocalDateTime value) throws IOException {
            if (value == null) { out.nullValue(); return; }
            out.value(F.format(value));
        }

        @Override
        public LocalDateTime read(JsonReader in) throws IOException {
            if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; }
            return LocalDateTime.parse(in.nextString(), F);
        }
    }

    // LocalDate ⇔ "2026-10-06"
    static class LocalDateAdapter extends TypeAdapter<LocalDate> {
        @Override
        public void write(JsonWriter out, LocalDate value) throws IOException {
            if (value == null) { out.nullValue(); return; }
            out.value(value.toString());
        }

        @Override
        public LocalDate read(JsonReader in) throws IOException {
            if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; }
            return LocalDate.parse(in.nextString());
        }
    }

    public static void main(String[] args) {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
                .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
                .create();

        Order order = new Order(1, LocalDateTime.of(2026, 10, 4, 9, 30, 0), LocalDate.of(2026, 10, 6));
        String json = gson.toJson(order);
        System.out.println(json);

        Order back = gson.fromJson(json, Order.class);
        System.out.println(back);

        Order nulls = gson.fromJson("{\"id\":2,\"orderedAt\":null}", Order.class);
        System.out.println(nulls);
    }
}

実行結果:

{"id":1,"orderedAt":"2026-10-04T09:30:00","shipDate":"2026-10-06"}
Order[id=1, orderedAt=2026-10-04T09:30, shipDate=2026-10-06]
Order[id=2, orderedAt=null, shipDate=null]

TypeAdapterは「この型をJSONのどんな値として書くか・読むか」を決める部品です。書き方の型は決まっていて、nullのときの分岐を write と read の両方に入れる のがポイントです。

java.util.Date はGsonが変換方法を持っていますが、既定の書式は "Oct 4, 2026, 9:30:00 AM" のような英語表記でした(実際の出力では、AMの前が幅の狭い空白文字)。setDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX") を指定すると "2026-10-04T09:30:00+09:00" になります。

OptionalをGsonで変換する

Optional をフィールドに持つクラスも、そのまま渡すと同じ種類の例外になります。Optional<String> nickname を持つクラスで toJson を呼んだときの結果です(fromJson でも同じメッセージでした)。

toJson  : com.google.gson.JsonIOException
Failed making field 'java.util.Optional#value' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type.
See https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible

対応は2通りあります。

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Optional;

public class OptOk {
    // 方法1:フィールドは String のまま、getter で Optional を返す
    static class Profile {
        String name = "佐藤";
        String nickname;
        Optional<String> nickname() { return Optional.ofNullable(nickname); }
    }

    // 方法2:Optional<T> 用の TypeAdapterFactory を登録する
    static class OptionalFactory implements TypeAdapterFactory {
        @Override
        @SuppressWarnings({"unchecked", "rawtypes"})
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            if (type.getRawType() != Optional.class) return null;
            Type inner = type.getType() instanceof ParameterizedType p
                    ? p.getActualTypeArguments()[0] : Object.class;
            TypeAdapter<Object> delegate = (TypeAdapter<Object>) gson.getAdapter(TypeToken.get(inner));
            return (TypeAdapter<T>) new TypeAdapter<Optional<?>>() {
                @Override
                public void write(JsonWriter out, Optional<?> value) throws IOException {
                    if (value == null || value.isEmpty()) { out.nullValue(); return; }
                    delegate.write(out, value.get());
                }
                @Override
                public Optional<?> read(JsonReader in) throws IOException {
                    if (in.peek() == JsonToken.NULL) { in.nextNull(); return Optional.empty(); }
                    return Optional.ofNullable(delegate.read(in));
                }
            };
        }
    }

    record Card(String name, Optional<String> nickname, Optional<Integer> age) {}

    public static void main(String[] args) {
        Gson plain = new Gson();
        Profile p = plain.fromJson("{\"name\":\"鈴木\"}", Profile.class);
        System.out.println("1: " + plain.toJson(p) + " / " + p.nickname());

        Gson gson = new GsonBuilder().registerTypeAdapterFactory(new OptionalFactory()).create();
        System.out.println("2: " + gson.toJson(new Card("佐藤", Optional.of("さとちゃん"), Optional.empty())));
        System.out.println("3: " + gson.fromJson("{\"name\":\"鈴木\",\"nickname\":\"すーさん\",\"age\":30}", Card.class));
        System.out.println("4: " + gson.fromJson("{\"name\":\"田中\",\"nickname\":null}", Card.class));
    }
}

実行結果:

1: {"name":"鈴木"} / Optional.empty
2: {"name":"佐藤","nickname":"さとちゃん"}
3: Card[name=鈴木, nickname=Optional[すーさん], age=Optional[30]]
4: Card[name=田中, nickname=Optional.empty, age=null]
方法 内容 向いている場面
方法1 フィールドは普通の型にして、メソッドでOptionalを返す 自分で直せるクラス。設定が要らず読みやすい
方法2 TypeAdapterFactory を登録する recordのコンポーネントなど、Optionalのまま持ちたいとき

方法2の4番の結果に注目してください。JSONのキーが null なら Optional.empty になりますが、キーそのものが無い age は null です。アダプタは「キーがあったとき」にだけ呼ばれるためです。キーが省かれるJSONを読むなら、方法1のほうが扱いやすくなります。

フィールドの初期値が消えるとき

int limit = 20; のようにフィールドへ初期値を書いたクラスで、JSONに limit が無い場合の結果です。引数なしコンストラクタの有無で変わりました。

クラスの形 {"name":"設定A"} を読んだ結果
引数なしコンストラクタあり limit=20 tags=[](初期値が残る)
引数なしコンストラクタなし limit=0 tags=null(初期値が入らない)

引数なしコンストラクタが無いと、Gsonはコンストラクタを通さずにインスタンスを作るため、フィールドの初期化も実行されません。公式のTroubleshootingにも「Default field values not present after deserialization」という項目があります。初期値を使うクラスには引数なしコンストラクタを用意するか、recordにするのが確実です。

例外メッセージと直し方の対応表

Gsonの例外は、メッセージの形から原因がほぼ特定できます。次の表は、入力と実際に出たメッセージをまとめたものです(fromJson の例外クラスは、注記のないものはすべて JsonSyntaxException)。

入力したJSON → 受ける型 実際のメッセージ 直し方
[{"name":"ノート"}] → Item.class Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path $ JSONは配列。TypeToken<List<Item>> か Item[].class で受ける
{"name":"ノート"} → List<Item> Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $ JSONはオブジェクト。Item.class で受けるか、配列を包むクラスを作る
{"name":{"ja":"ノート"}} → String name Expected a string but was BEGIN_OBJECT at line 1 column 10 path $.name name の型を、入れ子に合うクラスへ変える
{"tags":"文具"} → List<String> tags Expected BEGIN_ARRAY but was STRING at line 1 column 10 path $.tags フィールドの型をStringにするか、送る側を配列にそろえる
{"price":"百八十"} → int price NumberFormatException: For input string: "百八十" 数値に直せる値を送る。文字のまま持つならStringに
{"price":180.5} → int price NumberFormatException: Expected an int but was 180.5 at line 1 column 15 path $.price double か BigDecimal に変える
{"name":"ノート"(閉じ忘れ) EOFException: End of input at line 1 column 14 path $.name JSONが途中で切れている。送信元や読み込み範囲を確かめる
{"name":"ノート"} x MalformedJsonException: Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1 column 17 path $ 末尾の余分な文字を取り除く
LocalDateTime のフィールド JsonIOException: Failed making field 'java.time.LocalDateTime#date' accessible; … registerTypeAdapter でアダプタを登録
Optional のフィールド JsonIOException: Failed making field 'java.util.Optional#value' accessible; … フィールドを普通の型にする/Factoryを登録
List.class で読んで要素を取り出す ClassCastException: class com.google.gson.internal.LinkedTreeMap cannot be cast to class … TypeToken<List<Item>> を渡す
double がNaNのオブジェクトを toJson IllegalArgumentException: NaN is not a valid double value as per JSON specification. … 値を見直す。出したいなら serializeSpecialFloatingPointValues()

メッセージの読み方は次のとおりです。

  • Expected A but was B:Javaの型はAを待っていたが、JSONにはBが来た、という意味。BEGIN_OBJECT は {、BEGIN_ARRAY は [
  • path:JSONの中の場所。$ は一番外側、$.name は name キー
  • line / column:JSON文字列の中の位置

「Expected BEGIN_OBJECT but was BEGIN_ARRAY」は、Javaの型とJSONの形のどちらを合わせるかを決めれば解決します。外部のAPIから来るJSONなら、受け取った文字列をそのまま一度出力して、先頭が { か [ かを見るのが近道です。

同じコードで、例外にならなかった入力も3つありました。

入力 結果 覚えておくこと
空文字 "" 例外なしで null が返る 戻り値のnullチェックを入れる
クラスに無いキー(color) 例外なしで無視される 項目が増えても読み続けられる
{name:'ノート'}(引用符なしのキー) 例外なしで読める 厳密に検査するなら setStrictness(Strictness.STRICT)

GsonBuilderの設定一覧

設定を変えるときは new Gson() の代わりに new GsonBuilder()....create() を使います。次の表は、どれもGson 2.13.1で動かして結果を確かめたものです。

メソッド 役割 確認した結果
setPrettyPrinting() 改行とインデントを付ける 2スペースで整形される
serializeNulls() nullのフィールドも出力 "note":null が出る
setFieldNamingPolicy(...) キー名の変換規則 authorName が author_name に
excludeFieldsWithoutExposeAnnotation() @Expose 付きだけ対象 印の無い項目が外れる
disableHtmlEscaping() 山かっこ・&・=・引用符をそのまま出力 既定ではこれらがユニコードのエスケープ表記になる。指定すると "A<B & C='D'" のまま
setDateFormat(pattern) java.util.Date の書式 "2026-10-04T09:30:00+09:00"
registerTypeAdapter(型, アダプタ) 型ごとの変換を登録 LocalDateTimeを文字列で入出力
registerTypeAdapterFactory(factory) 型引数つきの型の変換を登録 Optionalを中身の値で入出力
serializeSpecialFloatingPointValues() NaN・無限大を出力 {"ratio":NaN}(既定は例外)
setLongSerializationPolicy(STRING) longを文字列で出力 {"big":"9007199254740993"}
setObjectToNumberStrategy(LONG_OR_DOUBLE) Object型で受ける数値の型 既定は 3 が 3.0(Double)。指定するとLong
setStrictness(Strictness.STRICT) JSONの文法を厳密に検査 引用符なしのキーが MalformedJsonException に
setVersion(1.0) @Since で項目を出し分け @Since(2.0) の項目が外れる
enableComplexMapKeySerialization() Mapのキーをオブジェクトとして出力 既定 {"[1, 2]":"A"} → [[[1,2],"A"]]
disableJdkUnsafe() コンストラクタを通さない生成を止める 引数なしコンストラクタの無いクラスで例外

なかでも setObjectToNumberStrategy は覚えておくと役立ちます。Map<String, Object> で受けると、既定では整数も 3.0 になり、9007199254740993 のような大きなIDは 9.007199254740992E15 と末尾が変わりました。ToNumberPolicy.LONG_OR_DOUBLE を指定すると、{count=3, rate=1.5, id=9007199254740993} と整数のまま読めます。

GsonとJacksonの比較表

同じデータを、Gson 2.13.1とJackson 2.16.1(jackson-databind・jackson-core・jackson-annotationsの3つのjarをクラスパスに置いて実行)に渡した結果です。

観点 Gson Jackson
依存 gson のjar 1つ 今回は jackson-databind・jackson-core・jackson-annotations の3つで実行
nullの出力(既定) 出力しない "memo":null と出力する
nullの切り替え serializeNulls() で出力 Include.NON_NULL で省略
未知のフィールド(既定) 無視して読み続ける UnrecognizedPropertyException(Unrecognized field "color" …)
未知のフィールドの切り替え 設定なしで無視 FAIL_ON_UNKNOWN_PROPERTIES をfalseに
LocalDateTime(既定) JsonIOException InvalidDefinitionException(Java 8 date/time type … not supported by default)
LocalDateTimeの対応 TypeAdapterを自分で書いて登録 jackson-datatype-jsr310 の JavaTimeModule を登録
record そのまま変換できた そのまま変換できた
Spring Bootでの既定 クラスパスにあれば Gson のBeanが自動設定される 既定のライブラリ

Spring Bootについては、公式リファレンス(Spring Boot 4.1.1の「JSON」のページ)に 「Jackson 3 is the preferred and default library」 と書かれています。同じページによると、GsonもクラスパスにあればBeanが自動で用意され、spring.gson.* のプロパティや GsonBuilderCustomizer で設定を変えられます。

こんなとき 選びやすいほう
小さなツールやバッチでJSONを読み書きしたい Gson(jar 1つ、設定なしで始められる)
項目が増えても読み続けたい Gson(未知のフィールドを既定で無視)
Spring BootでWeb APIを作る Jackson(既定のまま使える)
日時型を多く扱う Jackson(モジュールの登録で済む)
クラスを作らずにJSONを少しだけ読みたい Gson(JsonParser で手軽に読める)

どちらも定番のライブラリです。すでに入っているほうを使い、チーム内でそろえる のがいちばんの近道です。

よくある質問

Q. TypeTokenの末尾の波かっこは省略できますか? A. 省略しないでください。{} で無名クラスを作ることで、List<Item> の Item という型引数が実行時まで残ります。TypeToken.getParameterized(List.class, Item.class) を使えば、波かっこなしで同じ型を表せます。

Q. JSONに無い項目はどうなりますか? A. 例外にはならず、参照型はnull、プリミティブは0やfalseになります。フィールドに書いた初期値を使いたい場合は、引数なしコンストラクタを用意してください。recordでは常にnull/0/falseがコンストラクタに渡されます。

Q. fromJsonの結果がnullになるのはなぜですか? A. 入力が空文字のときは、例外ではなくnullが返りました。ファイルやHTTPの応答が空の可能性があるときは、戻り値のnullチェックを入れておくと安心です。

まとめ

  • 基本は toJson と fromJson の2つ。nullは出力されず、未知のキーは無視される
  • Listを読むときは new TypeToken<List<Item>>() {}。List.class だとLinkedTreeMapになり、取り出す行でClassCastException
  • recordはそのまま変換できる。コンパクトコンストラクタの検査も読み込み時に働く
  • LocalDateTimeとOptionalはJsonIOException。TypeAdapter/TypeAdapterFactoryを登録して直す
  • 「Expected A but was B」はJavaの型とJSONの形のずれ。pathで場所を特定できる
  • 設定はGsonBuilderに集める。setObjectToNumberStrategy で整数がDoubleになるのを防げる
  • Jacksonとの大きな違いは、nullの出力・未知フィールド・日時型の既定動作

参考・出典

  • Gson 公式README(依存の書き方・対応するJavaの版):https://github.com/google/gson/blob/main/README.md
  • Gson Troubleshooting Guide:https://github.com/google/gson/blob/main/Troubleshooting.md
  • Spring Boot リファレンス「JSON」:https://docs.spring.io/spring-boot/reference/features/json.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Gsonの使い方|toJson・fromJson・List変換」

株式会社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で一緒に開発しながら、 成長を楽しみませんか?