google/gson · error · IllegalStateException

Expected {expected} but was {peek}{location}

Error message

Expected {expected} but was {peek}{location}

What it means

JsonTreeReader.expect throws IllegalStateException('Expected ' + expected + ' but was ' + peek() + locationString) when the current token does not match the structural token the method requires. expect() is called by beginArray/endArray/beginObject/endObject/nextBoolean/nextNull, so any mismatch between what the caller asks for (e.g. beginArray) and what the tree actually has (e.g. an object) is rejected. The {expected} is the required JsonToken, {peek} the actual token, {location} the JSON path.

Source

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

      throw new MalformedJsonException(
          "Custom JsonElement subclass " + o.getClass().getName() + " is not supported");
    }
  }

  private Object peekStack() {
    return stack[stackSize - 1];
  }

  @CanIgnoreReturnValue
  private Object popStack() {
    Object result = stack[--stackSize];
    stack[stackSize] = null;
    return result;
  }

  private void expect(JsonToken expected) throws IOException {
    if (peek() != expected) {
      throw new IllegalStateException(
          "Expected " + expected + " but was " + peek() + locationString());
    }
  }

  private String nextName(boolean skipName) throws IOException {
    expect(JsonToken.NAME);
    Iterator<?> i = (Iterator<?>) peekStack();
    Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
    String result = (String) entry.getKey();
    pathNames[stackSize - 1] = skipName ? "<skipped>" : result;
    push(entry.getValue());
    return result;
  }

  @Override
  public String nextName() throws IOException {
    return nextName(false);
  }

View on GitHub (pinned to 8b8628c656)

Solutions

  1. Inspect the {expected}/{peek}/{location} in the message to find where the layout assumption diverges from the data.
  2. In custom adapters, branch on in.peek() rather than assuming a fixed token sequence.
  3. Validate the JSON structure (object vs array at each path) before deserializing, or use lenient manual navigation.
  4. Align the producer's data shape with the adapter, or update the adapter to match the new shape.

Example fix

// before: adapter assumes array but data is object
class Adapter extends TypeAdapter<List<X>> {
  public List<X> read(JsonReader in) throws IOException {
    in.beginArray(); // IllegalStateException if JSON is an object
    ...
  }
}

// after: branch on token
public List<X> read(JsonReader in) throws IOException {
  if (in.peek() == JsonToken.BEGIN_OBJECT) { /* read as map */ }
  else { in.beginArray(); /* read as list */ }
  ...
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Check the next token matches what your adapter assumes before calling begin*/end*
JsonToken t = reader.peek();
if (t != JsonToken.BEGIN_OBJECT) {
  throw new IllegalStateException("Expected object at " + reader.getPath() + ", got " + t);
}
reader.beginObject();

Type guard

static boolean isToken(JsonReader r, JsonToken expected) throws IOException {
  return r.peek() == expected;
}

Try / catch

try {
  reader.beginArray();
} catch (IllegalStateException e) {
  if (e.getMessage() != null && e.getMessage().startsWith("Expected ")) {
    throw new InvalidPayloadException("Structure mismatch at " + reader.getPath(), e);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling beginArray() when the current element is a JsonObject (or vice versa); endObject() when there is no open object; nextBoolean()/nextNull() when the token is not BOOLEAN/NULL. Typical of a custom TypeAdapter that assumes a layout that does not match the data.

Common situations: Custom adapter with hard-coded structure assumptions; data contract drift; wrong key ordering causing nextName misalignment; reusing a reader that's mid-structure.

Related errors


AI-assisted analysis of google/gson@8b8628c656 (2026-08-04). Data as JSON: /data/errors/4814083cb77b40cc.json. Report an issue: GitHub.