{"id":"4a8ad521d5ab3c44","repo":"square/retrofit","slug":"json-document-was-not-fully-consumed","errorCode":null,"errorMessage":"JSON document was not fully consumed.","messagePattern":"JSON document was not fully consumed\\.","errorType":"exception","errorClass":"JsonIOException","httpStatus":null,"severity":"error","filePath":"retrofit-converters/gson/src/main/java/retrofit2/converter/gson/GsonResponseBodyConverter.java","lineNumber":42,"sourceCode":"import okhttp3.ResponseBody;\nimport retrofit2.Converter;\n\nfinal class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {\n  private final Gson gson;\n  private final TypeAdapter<T> adapter;\n\n  GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {\n    this.gson = gson;\n    this.adapter = adapter;\n  }\n\n  @Override\n  public T convert(ResponseBody value) throws IOException {\n    JsonReader jsonReader = gson.newJsonReader(value.charStream());\n    try {\n      T result = adapter.read(jsonReader);\n      if (jsonReader.peek() != JsonToken.END_DOCUMENT) {\n        throw new JsonIOException(\"JSON document was not fully consumed.\");\n      }\n      return result;\n    } finally {\n      value.close();\n    }\n  }\n}\n","sourceCodeStart":24,"sourceCodeEnd":50,"githubUrl":"https://github.com/square/retrofit/blob/d0b112dad073b7fe49c953ebc46ff1b424cb1e51/retrofit-converters/gson/src/main/java/retrofit2/converter/gson/GsonResponseBodyConverter.java#L24-L50","documentation":"JsonIOException thrown by GsonResponseBodyConverter when, after the TypeAdapter finishes reading a value, the JsonReader is not positioned at END_DOCUMENT (line 41). This means the response body contained trailing content after the first complete JSON value — Gson requires the body to be exactly one JSON value. The converter enforces this strictly to surface malformed responses and buggy custom adapters rather than silently dropping data.","triggerScenarios":"Server returns concatenated JSON values (`{\"a\":1}{\"b\":2}`), trailing garbage after the JSON (`{\"a\":1}<<<`), an HTML error page with embedded JSON, a BOM or whitespace-bounded extra tokens, or a custom TypeAdapter that returns early without consuming the full token stream.","commonSituations":"Backend bug appending a second object or a debug log line after the JSON; a transparent proxy/CDN injecting content; mismatched Content-Type serving an HTML error page; a custom Gson TypeAdapter that does not consume nested tokens; chunked/transfer-encoding artifacts; API version change adding a wrapper the client does not expect.","solutions":["Log the raw body with an OkHttp HttpLoggingInterceptor at BODY level (non-production) to see the trailing content.","Fix the server to return exactly one JSON value with no trailing data.","If trailing content is expected/unavoidable, register a custom Converter.Factory that reads only the first value, or a TypeAdapter that consumes the remainder.","If a custom TypeAdapter is in use, ensure it reads every token (do not return while peek() is not END_OBJECT/END_ARRAY)."],"exampleFix":"// before (server returns: {\"id\":1}GARBAGE)\n// client throws JsonIOException: JSON document was not fully consumed.\n\n// after — custom lenient converter reads only the first JSON value\nfinal class LenientGsonConverterFactory extends Converter.Factory {\n  private final Gson gson;\n  LenientGsonConverterFactory(Gson gson) { this.gson = gson; }\n  @Override public Converter<ResponseBody, ?> responseBodyConverter(\n      Type type, Annotation[] anns, Retrofit rf) {\n    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));\n    return body -> {\n      try (JsonReader reader = gson.newJsonReader(body.charStream())) {\n        reader.setLenient(true);\n        @SuppressWarnings(\"unchecked\") Object result = ((TypeAdapter<Object>) adapter).read(reader);\n        return result; // ignore any trailing content\n      } finally { body.close(); }\n    };\n  }\n}","handlingStrategy":"try-catch","validationCode":"// Inspect the raw body once (non-production) to confirm it is a single JSON value.\n// Note: a ResponseBody can be read only once, so this is for diagnostics, not a\n// per-request guard. Use an OkHttp interceptor in debug builds:\nHttpLoggingInterceptor logger = new HttpLoggingInterceptor(msg -> Log.d(\"HTTP\", msg));\nlogger.setLevel(HttpLoggingInterceptor.Level.BODY);\nOkHttpClient client = new OkHttpClient.Builder().addInterceptor(logger).build();","typeGuard":null,"tryCatchPattern":"// JsonIOException is an IOException; handle it at the call site alongside network errors.\nservice.getUser(id).enqueue(new Callback<User>() {\n  @Override public void onResponse(Call<User> c, Response<User> r) { /* ... */ }\n  @Override public void onFailure(Call<User> c, Throwable t) {\n    if (t instanceof com.google.gson.JsonIOException) {\n      // body had trailing content / multiple JSON values; log and report a\n      // contract violation rather than retrying blindly\n      reportMalformedResponse(t);\n    } else {\n      // ordinary IOException (network, timeout)\n    }\n  }\n});","preventionTips":["Log full response bodies at BODY level in staging to catch trailing content early.","Write contract tests against the real backend response fixtures (not just hand-built JSON).","If you ship custom Gson TypeAdapters, ensure they consume every nested token before returning.","Have the backend declare Content-Type: application/json and avoid appending non-JSON data."],"tags":["gson","json","parsing","network","retrofit"],"analyzedSha":"d0b112dad073b7fe49c953ebc46ff1b424cb1e51","analyzedAt":"2026-08-04T19:12:59.096Z","schemaVersion":2}