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

  1. In custom adapters, switch on in.peek() and only call nextString() when token is STRING (or NUMBER).
  2. Handle NULL with in.nextNull(), and other shapes explicitly, before reading a string.
  3. Align the producer to emit the field as a string consistently.
  4. 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

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


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