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

  1. Do not share a single JsonReader between threads; create a fresh reader per parse.
  2. Avoid subclassing JsonReader with inconsistent peek/next semantics.
  3. Use Gson.fromJson rather than invoking JsonElementTypeAdapter directly.
  4. 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

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

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)