google/gson · error · IllegalStateException
Unexpected token
Error message
Unexpected token: ${peeked} What it means
JsonElementTypeAdapter.readTerminal only handles STRING, NUMBER, BOOLEAN, NULL. Reaching the default branch means the JsonReader reported a structural token (BEGIN_ARRAY/BEGIN_OBJECT/END_*) where a terminal value was expected, indicating the reader is in an inconsistent/corrupt state. This is normally only reachable through internal misuse or a buggy custom JsonReader.
Solutions
- Do not share a single JsonReader between threads; create a fresh reader per parse.
- Avoid subclassing JsonReader with inconsistent peek/next semantics.
- Use Gson.fromJson rather than invoking JsonElementTypeAdapter directly.
- File a Gson bug if the reader is a standard JsonReader and the failure is reproducible.
Example fix
// before: shared reader mutated concurrently
val reader = JsonReader(StringReader(json))
thread { JsonElementTypeAdapter.ADAPTER.read(reader) }
JsonElementTypeAdapter.ADAPTER.read(reader) // IllegalStateException
// after: one reader per parse, single-threaded
val reader = JsonReader(StringReader(json))
val el = JsonElementTypeAdapter.ADAPTER.read(reader) Defensive patterns
Strategy: try-catch
Validate before calling
// Do not invoke JsonElementTypeAdapter directly; use Gson
// If you must, ensure the reader is single-threaded and at a valid token
JsonToken t = reader.peek();
if (t == BEGIN_ARRAY || t == BEGIN_OBJECT || t == STRING || t == NUMBER || t == BOOLEAN || t == NULL) {
JsonElement e = JsonElementTypeAdapter.ADAPTER.read(reader);
} Try / catch
try {
JsonElement e = JsonElementTypeAdapter.ADAPTER.read(reader);
} catch (IllegalStateException ex) {
if (ex.getMessage().startsWith("Unexpected token")) {
// reader in inconsistent state; reset and re-parse
throw new CorruptedReaderException(ex);
}
throw ex;
} Prevention
- Never share a JsonReader across threads.
- Prefer gson.fromJson over direct adapter calls.
- Do not subclass JsonReader with inconsistent semantics.
- Consume each reader exactly once.
When it happens
Trigger: Calling JsonElementTypeAdapter.ADAPTER.read on a JsonReader whose peek() returns a structural token after the adapter already decided it is a terminal (e.g. concurrent mutation of the reader, or a custom JsonReader implementation returning inconsistent tokens).
Common situations: Sharing a JsonReader across threads; subclassing JsonReader incorrectly; calling internal adapter methods directly instead of via Gson.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
AI-assisted analysis of google/gson@310ac341f2 (2026-08-10).
Data as JSON: /api/errors/4c0fe24489f03430.
Report an issue: GitHub.
Appendix: source
Thrown at gson/src/main/java/com/google/gson/internal/bind/JsonElementTypeAdapter.java:72
}
}
/** Reads a {@link JsonElement} which cannot have any nested elements */
private JsonElement readTerminal(JsonReader in, JsonToken peeked) throws IOException {
switch (peeked) {
case STRING:
return new JsonPrimitive(in.nextString());
case NUMBER:
String number = in.nextString();
return new JsonPrimitive(new LazilyParsedNumber(number));
case BOOLEAN:
return new JsonPrimitive(in.nextBoolean());
case NULL:
in.nextNull();
return JsonNull.INSTANCE;
default:
// When read(JsonReader) is called with JsonReader in invalid state
throw new IllegalStateException("Unexpected token: " + peeked);
}
}
@Override
public JsonElement read(JsonReader in) throws IOException {
// Optimization if value already exists as JsonElement
if (in instanceof JsonTreeReader) {
return ((JsonTreeReader) in).nextJsonElement();
}
// Either JsonArray or JsonObject
JsonElement current;
JsonToken peeked = in.peek();
current = tryBeginNesting(in, peeked);
if (current == null) {
return readTerminal(in, peeked);
}View on GitHub (pinned to 310ac341f2)