google/gson · error · IllegalStateException

Unexpected token: ${peeked}

Error message

Unexpected token: ${peeked}

What it means

ObjectTypeAdapter.readTerminal() only accepts STRING, NUMBER, BOOLEAN, and NULL. For any other token (NAME, END_ARRAY, END_OBJECT, END_DOCUMENT) it throws IllegalStateException("Unexpected token: ..."). The comment in source notes this happens 'when read(JsonReader) is called with JsonReader in invalid state' - typically a bug in a custom adapter that mis-advances the reader before delegating to Object deserialization, rather than malformed user JSON per se.

Source

Thrown at gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java:104

        return null;
    }
  }

  /** Reads an {@code Object} which cannot have any nested elements */
  private Object readTerminal(JsonReader in, JsonToken peeked) throws IOException {
    switch (peeked) {
      case STRING:
        return in.nextString();
      case NUMBER:
        return toNumberStrategy.readNumber(in);
      case BOOLEAN:
        return in.nextBoolean();
      case NULL:
        in.nextNull();
        return null;
      default:
        // When read(JsonReader) is called with JsonReader in invalid state
        throw new IllegalStateException("Unexpected token: " + peeked);
    }
  }

  @Override
  public Object read(JsonReader in) throws IOException {
    // Either List or Map
    Object current;
    JsonToken peeked = in.peek();

    current = tryBeginNesting(in, peeked);
    if (current == null) {
      return readTerminal(in, peeked);
    }

    Deque<Object> stack = new ArrayDeque<>();

    while (true) {
      while (in.hasNext()) {

View on GitHub (pinned to 310ac341f2)

Solutions

  1. Ensure the JsonReader is positioned at a value token before delegating to Object deserialization
  2. Do not call nextName()/beginObject() and then pass the reader to ObjectTypeAdapter without proper positioning
  3. Validate JSON structure (or unit-test the adapter) to confirm the reader state at delegation points
Defensive patterns

Strategy: validation

Validate before calling

// Before delegating to Object deserialization, ensure the reader is at a value token
JsonToken t = reader.peek();
if (t == JsonToken.NAME || t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT || t == JsonToken.END_DOCUMENT) {
    throw new IllegalStateException("Reader not positioned at a value: " + t);
}
Object o = gson.getAdapter(Object.class).read(reader);

Try / catch

try {
  Object o = gson.fromJson(reader, Object.class);
} catch (IllegalStateException e) {
  // 'Unexpected token: <token>': reader was at a structural token; reposition before retry
}

Prevention

When it happens

Trigger: A custom TypeAdapter that calls nextName()/beginObject() and then delegates the reader to Object deserialization without leaving it positioned at a value; calling fromJson where the reader is at a structural token rather than a value; misusing the reader API in streaming code.

Common situations: Custom adapter that consumes the wrong tokens; incorrect manual JsonReader usage; a TypeAdapter that partially reads an object then hands off to Gson's Object adapter.

Understand the failure class

Related errors


AI-assisted analysis of google/gson@310ac341f2 (2026-08-10). Data as JSON: /api/errors/5a03680c38e8b5cd. Report an issue: GitHub.