square/retrofit · error · JsonIOException
JSON document was not fully consumed.
Error message
JSON document was not fully consumed.
What it means
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.
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).
Example fix
// before (server returns: {"id":1}GARBAGE)
// client throws JsonIOException: JSON document was not fully consumed.
// after — custom lenient converter reads only the first JSON value
final class LenientGsonConverterFactory extends Converter.Factory {
private final Gson gson;
LenientGsonConverterFactory(Gson gson) { this.gson = gson; }
@Override public Converter<ResponseBody, ?> responseBodyConverter(
Type type, Annotation[] anns, Retrofit rf) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return body -> {
try (JsonReader reader = gson.newJsonReader(body.charStream())) {
reader.setLenient(true);
@SuppressWarnings("unchecked") Object result = ((TypeAdapter<Object>) adapter).read(reader);
return result; // ignore any trailing content
} finally { body.close(); }
};
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// Inspect the raw body once (non-production) to confirm it is a single JSON value.
// Note: a ResponseBody can be read only once, so this is for diagnostics, not a
// per-request guard. Use an OkHttp interceptor in debug builds:
HttpLoggingInterceptor logger = new HttpLoggingInterceptor(msg -> Log.d("HTTP", msg));
logger.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(logger).build(); Try / catch
// JsonIOException is an IOException; handle it at the call site alongside network errors.
service.getUser(id).enqueue(new Callback<User>() {
@Override public void onResponse(Call<User> c, Response<User> r) { /* ... */ }
@Override public void onFailure(Call<User> c, Throwable t) {
if (t instanceof com.google.gson.JsonIOException) {
// body had trailing content / multiple JSON values; log and report a
// contract violation rather than retrying blindly
reportMalformedResponse(t);
} else {
// ordinary IOException (network, timeout)
}
}
}); Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- gson == null
- CompletableFuture return type must be parameterized as…
- context == null
- error == null
- error == null
AI-assisted analysis of square/retrofit@d0b112dad0 (2026-08-04).
Data as JSON: /api/errors/4a8ad521d5ab3c44.
Report an issue: GitHub.
Appendix: source
Thrown at retrofit-converters/gson/src/main/java/retrofit2/converter/gson/GsonResponseBodyConverter.java:42
import okhttp3.ResponseBody;
import retrofit2.Converter;
final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final Gson gson;
private final TypeAdapter<T> adapter;
GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override
public T convert(ResponseBody value) throws IOException {
JsonReader jsonReader = gson.newJsonReader(value.charStream());
try {
T result = adapter.read(jsonReader);
if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
throw new JsonIOException("JSON document was not fully consumed.");
}
return result;
} finally {
value.close();
}
}
}
View on GitHub (pinned to d0b112dad0)