はじめに (対象読者・この記事でわかること)

この記事は、Androidアプリ開発に携わっているJavaエンジニア、特にRetrofitやGson、RxAndroidを使ったネットワーク実装を行っている方を対象としています。実装中にJsonSyntaxException が発生し、原因が分からずにデバッグが止まってしまった経験がある方は必見です。本記事を読むことで、例外がスローされる具体的なシチュエーション、典型的なJSON構造のミスマッチ、そして実際のコード修正例を通して、例外を防止・解決できるようになります。背景にある概念や、開発現場でよくある落とし穴も併せて整理しているため、今後同様のエラーに遭遇した際の迅速な対処が可能になります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。
- Java の基本文法と Android アプリ開発の基礎
- Retrofit の基本的な使い方(インターフェース定義、Call と Observable の違い)
- Gson のシリアライズ/デシリアライズの概念
- RxJava / RxAndroid の Observable と Scheduler の基本

背景と問題点:Retrofit+Gson+RxAndroidでのPOST通信で起きるJsonSyntaxException

Retrofit は HTTP クライアントとして非常に便利ですが、内部ではレスポンスボディをパースする際に GsonConverterFactory を利用します。そのため、サーバーから返ってくる JSON が Gson が期待する型 と一致しないと com.google.gson.JsonSyntaxException がスローされます。

典型的なケースは次の通りです。

  1. データ構造の不一致
    - サーバー側が null を返すフィールドが、Java 側では intboolean のプリミティブ型で宣言されている。
    - 配列やリストが実際にはオブジェクトの配列であるべきところ、単一オブジェクトが返ってくる。

  2. 日付・時間フォーマットの不一致
    - Gson のデフォルト設定では ISO8601 以外の文字列を java.util.Date に変換できず、例外が発生する。

  3. 不正な JSON
    - サーバー側のエラーレスポンスが HTML になっている、または文字エンコードが UTF-8 でない。

さらに RxAndroid を介した非同期処理にすると、例外が onError のコールバックへ流れ、スタックトレースが見にくくなることがあります。したがって、例外の根本原因を把握するには、レスポンスの生データ をログに出力し、期待する POJO と照らし合わせる作業が重要です。

実装手順とエラー解決までのフロー

以下では、実際に Android Studio でプロジェクトを構成し、例外が出たときのデバッグ手順と修正例を示します。

ステップ1:Retrofit と Gson の設定を見直す

Java
// build.gradle (app) implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'io.reactivex.rxjava3:rxjava:3.1.5' implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
Java
// RetrofitClient.java public class RetrofitClient { private static final String BASE_URL = "https://api.example.com/"; private static Retrofit retrofit = null; public static Retrofit getInstance() { if (retrofit == null) { Gson gson = new GsonBuilder() .setLenient() // 文字列が不完全でも例外を抑制 .serializeNulls() // null フィールドもシリアライズ対象に .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") // 必要に応じてカスタム日付フォーマット .create(); retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) .build(); } return retrofit; } }

ポイントは setLenient()serializeNulls()、そして setDateFormat() を利用して、Gson が厳密すぎて例外を投げるケースを緩和することです。

ステップ2:API インターフェースの定義と POJO の整合性を確認

Java
// ApiService.java public interface ApiService { @POST("users/create") @Headers("Content-Type: application/json") Single<Response<UserResponse>> createUser(@Body CreateUserRequest request); }
Java
// CreateUserRequest.java public class CreateUserRequest { private String name; private String email; private Integer age; // int -> Integer に変更して null を許容 // getter / setter }
Java
// UserResponse.java public class UserResponse { private String id; private String name; private String email; private Integer age; // サーバーが age を省略した場合でも null が入る private Date createdAt; // カスタム日付フォーマットに合わせる // getter / setter }

注意点
- プリミティブ型 (int, boolean) は null を受け取れないため、必ず ラッパークラス (Integer, Boolean) に置き換える。
- フィールド名と JSON キーが違う場合は @SerializedName("json_key") アノテーションを付与する。

ハマった点やエラー解決

ケース A:サーバーが age を省略したときの例外

現象
JsonSyntaxException: Expected a int but was NULL at line ...

原因
CreateUserRequestint age; がプリミティブ型のため、サーバー側が null(省略)を返すとパースに失敗する。

解決策
intInteger に変更し、serializeNulls() を有効にした上で、リクエスト側でも age を省略可能にする。

ケース B:日付文字列のフォーマット違い

現象
JsonSyntaxException: java.text.ParseException: Unparseable date: "2025/09/15 12:34:56"

原因
Gson がデフォルトで期待する ISO8601 形式と、サーバーが返す yyyy/MM/dd HH:mm:ss が不一致。

解決策
GsonBuilder().setDateFormat("yyyy/MM/dd HH:mm:ss") を利用してフォーマットを合わせる。もしくは、@JsonAdapter を使ってカスタムデシリアライザを実装する。

Java
public class DateDeserializer implements JsonDeserializer<Date> { private final SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); @Override public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { try { return format.parse(json.getAsString()); } catch (ParseException e) { throw new JsonParseException(e); } } } // Gson 設定に追加 Gson gson = new GsonBuilder() .registerTypeAdapter(Date.class, new DateDeserializer()) .create();

ケース C:エラーレスポンスが HTML

現象
JsonSyntaxException: Expected BEGIN_OBJECT but was STRING at line ...

原因
サーバー側で 500 エラーが発生し、HTML エラーページが返ってきた。Gson は JSON とみなせず例外を投げる。

解決策
OkHttpInterceptor を導入し、ステータスコードが 200 以外のときはレスポンスボディを文字列で取得し、ログに出力する。必要に応じて ResponseBody を別の DTO にマッピングする。

Java
public class ErrorLoggingInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = chain.proceed(request); if (!response.isSuccessful()) { String errorBody = response.body() != null ? response.body().string() : "null"; Log.e("API_ERROR", "Code: " + response.code() + ", Body: " + errorBody); } return response; } }

完全な実装例

Java
public class UserRepository { private final ApiService apiService; public UserRepository() { apiService = RetrofitClient.getInstance().create(ApiService.class); } public Single<UserResponse> createUser(CreateUserRequest request) { return apiService.createUser(request) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .map(response -> { if (response.isSuccessful() && response.body() != null) { return response.body(); } else { // エラーハンドリングを統一 throw new RuntimeException("API error: " + response.code()); } }); } }

この構成で、JSON 形式の不整合・日付フォーマット・null 値の扱い をすべてカバーでき、JsonSyntaxException が発生する確率は大幅に低減します。

まとめ

本記事では、Retrofit・Gson・RxAndroid を組み合わせた POST 通信で頻出する JsonSyntaxException の原因と、具体的な対策を解説しました。

  • 原因の把握:JSON と POJO の型不一致、日付フォーマット、サーバー側エラーレスポンスの三大パターン
  • 設定の見直し:Gson の Lenient モード、null 許容、日付フォーマットのカスタマイズ
  • コードレベルの修正:プリミティブ型をラッパー型へ、@SerializedName の活用、カスタムデシリアライザの実装、OkHttp Interceptor によるエラーログ取得

これらを実践すれば、開発中に遭遇する JsonSyntaxException を素早く特定・解決でき、安定したネットワーク層の構築が可能になります。次回は、Retrofit のレスポンスキャッシュとオフライン対応 について掘り下げる予定です。

参考資料