google/gson · error · IllegalStateException
Expected STRING but was {token}{location}
Error message
Expected STRING but was {token}{location} What it means
JsonTreeReader.nextString throws IllegalStateException('Expected STRING but was ' + token + locationString) when the current token is neither STRING nor NUMBER. nextString accepts NUMBER as a convenience (it stringifies the number), but rejects BOOLEAN, NULL, NAME, BEGIN_*, END_*. The {token} is the actual JsonToken, {location} the path.
Source
Thrown at gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java:210
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);
}
@Override
public String nextString() throws IOException {
JsonToken token = peek();
if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
throw new IllegalStateException(
"Expected " + JsonToken.STRING + " but was " + token + locationString());
}
String result = ((JsonPrimitive) popStack()).getAsString();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
return result;
}
@Override
public boolean nextBoolean() throws IOException {
expect(JsonToken.BOOLEAN);
boolean result = ((JsonPrimitive) popStack()).getAsBoolean();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
return result;
}View on GitHub (pinned to 8b8628c656)
Solutions
- In custom adapters, switch on in.peek() and only call nextString() when token is STRING (or NUMBER).
- Handle NULL with in.nextNull(), and other shapes explicitly, before reading a string.
- Align the producer to emit the field as a string consistently.
- Use Gson.getAdapter(JsonElement.class).read(in) to capture the value as a tree, then inspect its type safely.
Example fix
// before
String name = in.nextString(); // throws if value is null or boolean
// after
String name;
if (in.peek() == JsonToken.NULL) { in.nextNull(); name = null; }
else if (in.peek() == JsonToken.STRING) { name = in.nextString(); }
else { throw new JsonSyntaxException("Expected name string at " + in.getPath()); } Defensive patterns
Strategy: type-guard
Validate before calling
JsonToken t = reader.peek();
if (t != JsonToken.STRING && t != JsonToken.NUMBER) {
throw new IllegalStateException("Expected string at " + reader.getPath() + ", got " + t);
}
String s = reader.nextString(); Type guard
static boolean isStringOrNumberToken(JsonReader r) throws IOException {
JsonToken t = r.peek();
return t == JsonToken.STRING || t == JsonToken.NUMBER;
} Try / catch
try {
return reader.nextString();
} catch (IllegalStateException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Expected STRING")) {
throw new InvalidPayloadException("Expected string field at " + reader.getPath(), e);
}
throw e;
} Prevention
- Handle NULL and other token types explicitly before calling nextString().
- Pin the data contract for string fields (always a string, or document nullable).
- Prefer reading into JsonElement and inspecting when the shape varies.
When it happens
Trigger: Calling nextString() on a JsonReader/JsonTreeReader positioned at a boolean, null, object, or array. Common in custom adapters or when deserializing a field whose value type changed from string to object/null.
Common situations: Field that was a string is now an object or null in a new API version; union types where the value can be a string or a structured object; lenient inputs like 'true'/'false' expected as strings.
Related errors
- Expected {expected} but was {peek}{location}
- Expected NUMBER but was {token}{location}
- JsonReader is closed
- JSON forbids NaN and infinities: {result}
- GSON ({GsonBuildConfig.VERSION}) cannot handle {type}
AI-assisted analysis of google/gson@8b8628c656 (2026-08-04).
Data as JSON: /data/errors/6a684f6f2d771efe.json.
Report an issue: GitHub.