lysine-dev/retrofit · error · JsonIOException

JSON document was not fully consumed.

Error message

JSON document was not fully consumed.

What it means

Thrown by GsonResponseBodyConverter.convert() as a JsonIOException after the registered TypeAdapter finishes reading. The converter reads one JSON value from the response body, then peeks the JsonReader; if the next token is not END_DOCUMENT (i.e. there is trailing content after the top-level value), it refuses to return a partial/ambiguous result and throws. This guards against silently ignoring malformed or concatenated JSON payloads.

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)

Solutions

  1. Inspect the raw response body (e.g. via an OkHttp logging interceptor at BODY level) to identify the trailing content and fix the server/proxy emitting it.
  2. If concatenated documents are intentional, switch to a streaming reader that loops on peek()!=END_DOCUMENT instead of using GsonResponseBodyConverter.
  3. If a custom TypeAdapter is leaving the reader mid-value, fix the adapter to fully consume its token range.
  4. Disable any server debug/echo flags or proxy body-rewriting that appends content after the JSON payload.

Example fix

// before — server returns: {"id":1}{"id":2}
// converter throws after reading the first object.

// after — fix the endpoint to return a single JSON value or array:
[{"id":1},{"id":2}]
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate the body shape before handing it to Retrofit/Gson when the source is suspect.
// This is only for non-Retrofit call paths; Retrofit owns ResponseBody reading internally.
String body = response.body().string();
JsonReader reader = new JsonReader(new StringReader(body));
new Gson().getAdapter(SomeType.class).read(reader);
if (reader.peek() != JsonToken.END_DOCUMENT) {
  log.warn("Trailing JSON content detected; investigate the server response.");
}

Try / catch

// Wrap the converter call (typically inside a custom Converter or interceptor) and surface a
// parse error with context rather than letting JsonIOException escape raw.
try {
  return converter.convert(response.body());
} catch (JsonIOException e) {
  if ("JSON document was not fully consumed.".equals(e.getMessage())) {
    throw new IllegalStateException("Server returned trailing JSON content; see raw body logs", e);
  }
  throw e;
}

Prevention

When it happens

Trigger: An HTTP response body that contains extra data after the single expected JSON value: multiple concatenated JSON objects, a JSON value followed by trailing characters/whitespace-with-content, a server streaming multiple documents into one body, or a proxy that appended diagnostics after the JSON.

Common situations: A backend that emits `{...}{...}` (two objects) in a single response, a misconfigured CDN/edge proxy appending analytics or error HTML, a debug-mode server echoing the request body after the response JSON, or a partial read by a custom TypeAdapter that consumed only part of the value.

Related errors


AI-assisted analysis of lysine-dev/retrofit@d0b112dad0 (2026-08-13). Data as JSON: /api/errors/c6a54bcdfccdccf3. Report an issue: GitHub.