google/gson · error · IllegalStateException

Unexpected when reading a JsonElement.

Error message

Unexpected ${peeked} when reading a JsonElement.

What it means

JsonTreeReader.nextJsonElement (used by JsonElementTypeAdapter when reading from a JsonElement tree) requires the current token to be a value or the start of an array/object. If peek() returns NAME, END_ARRAY, END_OBJECT, or END_DOCUMENT, it throws IllegalStateException. This signals the read was attempted at a position where no complete element can be produced.

Solutions

  1. Only read a JsonElement when the reader is positioned at a value or container start (check peek() first).
  2. Create a fresh JsonTreeReader from the JsonElement for each parse rather than reusing.
  3. Avoid mixing manual token consumption with JsonElement extraction on the same reader.
  4. If you need the whole tree, use the JsonElement directly instead of re-reading through a JsonTreeReader.

Example fix

// before: reader already consumed past the value
val reader = JsonTreeReader(tree)
reader.beginObject(); reader.nextName()
JsonElementTypeAdapter.ADAPTER.read(reader) // Unexpected NAME when reading a JsonElement

// after: read element before advancing into object structure
val reader = JsonTreeReader(tree)
val el = JsonElementTypeAdapter.ADAPTER.read(reader)
Defensive patterns

Strategy: validation

Validate before calling

// Position the reader at a value/container before extracting a JsonElement
JsonToken t = reader.peek();
if (t == NAME || t == END_ARRAY || t == END_OBJECT || t == END_DOCUMENT) {
  throw new IllegalStateException("Cannot read JsonElement at " + t);
}
JsonElement e = JsonElementTypeAdapter.ADAPTER.read(reader);

Try / catch

try {
  reader.nextJsonElement();
} catch (IllegalStateException e) {
  if (e.getMessage().startsWith("Unexpected ") && e.getMessage().contains("when reading a JsonElement")) {
    // reader mis-positioned; reset to a fresh JsonTreeReader
    throw new IllegalStateException("Re-read from a new reader", e);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling gson.fromJson(jsonElement, JsonElement.class) or otherwise reading a JsonElement out of a JsonTreeReader positioned at a name token or an end-of-container token. Reproducible by re-reading a tree at the wrong cursor position.

Common situations: Reusing a JsonTreeReader past its valid cursor; calling nextJsonElement after hasNext() returned false; integrating JsonElement trees with custom adapters that mis-position the reader.

Related errors


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

Appendix: source

Thrown at gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java:317

    try {
      result = primitive.getAsInt();
    } catch (NumberFormatException e) {
      throw numberFormatException("Expected an int but was " + primitive.getAsString(), e);
    }
    popStack();
    if (stackSize > 0) {
      pathIndices[stackSize - 1]++;
    }
    return result;
  }

  JsonElement nextJsonElement() throws IOException {
    JsonToken peeked = peek();
    if (peeked == JsonToken.NAME
        || peeked == JsonToken.END_ARRAY
        || peeked == JsonToken.END_OBJECT
        || peeked == JsonToken.END_DOCUMENT) {
      throw new IllegalStateException("Unexpected " + peeked + " when reading a JsonElement.");
    }
    JsonElement element = (JsonElement) peekStack();
    skipValue();
    return element;
  }

  @Override
  public void close() throws IOException {
    stack = new Object[] {SENTINEL_CLOSED};
    stackSize = 1;
  }

  @Override
  public void skipValue() throws IOException {
    JsonToken peeked = peek();
    switch (peeked) {
      case NAME:
        @SuppressWarnings("unused")

View on GitHub (pinned to 310ac341f2)