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
- Align the target type with the actual JSON structure (List vs single object).
- Register a custom TypeAdapter that tolerates both shapes (singleton-or-array pattern).
- Inspect the reported path and token to localize the mismatch.
- 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
- Keep producer and consumer schemas in sync.
- Handle singleton-or-array patterns with a tolerant custom adapter.
- Peek at the JSON shape before choosing the target type.
- Add contract tests asserting array-vs-object shapes.
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
- Expected STRING but was ${token} at path ${path}
- Expected NUMBER but was ${token} at path ${path}
- cannot deserialize ${baseType} because it does not define a
- cannot deserialize ${baseType} subtype named ${label}; did y
- Failed to parse date [${input}]: ${fail.getMessage()}
AI-assisted analysis of google/gson@310ac341f2 (2026-08-10).
Data as JSON: /api/errors/ce370c6748abe1e4.
Report an issue: GitHub.