google/gson · error · IllegalStateException

Expected ${expected} but was ${peek} at path ${path}

Error message

Expected ${expected} but was ${peek} at path ${path}

What it means

JsonTreeReader.expect compares the next token to an expected value (used by beginArray/endArray/beginObject/endObject/nextBoolean/nextNull). A mismatch throws IllegalStateException listing expected vs. actual token and the JSON path. This is the structural 'you said array but it's an object' failure for tree-backed parsing.

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 310ac341f2)

Solutions

  1. Align the target type with the actual JSON structure (List vs single object).
  2. Register a custom TypeAdapter that tolerates both shapes (singleton-or-array pattern).
  3. Inspect the reported path and token to localize the mismatch.
  4. Add schema validation or contract tests between producer and consumer.

Example fix

// before: JSON is {"items":{...}}
Gson().fromJson(json, Array<Items>::class.java) // Expected BEGIN_ARRAY but was BEGIN_OBJECT

// after
data class Wrapper(val items: Items)
Gson().fromJson(json, Wrapper::class.java)
Defensive patterns

Strategy: validation

Validate before calling

// Peek at structure before binding to a type
JsonElement tree = JsonParser.parseString(json);
if (tree.isJsonObject()) gson.fromJson(tree, Single.class);
else if (tree.isJsonArray()) gson.fromJson(tree, ListOf.class);
// or accept both via a tolerant adapter

Type guard

static boolean isArrayShape(JsonElement e) { return e.isJsonArray(); }
static boolean isObjectShape(JsonElement e) { return e.isJsonObject(); }

Try / catch

try {
  gson.fromJson(reader, List.class);
} catch (IllegalStateException e) {
  if (e.getMessage().startsWith("Expected ") && e.getMessage().contains(" but was ")) {
    // structural mismatch; inspect token and retry with correct type
    log.warn("Shape mismatch: {}", e.getMessage());
  }
  throw e;
}

Prevention

When it happens

Trigger: Deserializing a type whose adapter calls beginArray() when the JSON object actually starts with '{', or endObject() when not at END_OBJECT. Common when a field type changes between producer and consumer (array vs object).

Common situations: Schema drift where a singleton object becomes an array (or vice-versa); polymorphic payloads; wrong target type passed to fromJson; element fed to JsonTreeReader that mismatches the expected shape.

Related errors


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