grpc/grpc-java · error · ClassCastException
Number expected to be integer:
Error message
Number expected to be integer:
What it means
getNumberAsInteger reads a numeric value from a parsed JSON config map and casts it to int. If the stored value is a Double with a fractional part (e.g. 1.5), the int cast would silently truncate, so gRPC throws a ClassCastException instead. It exists to catch lossy numeric conversions in service config parsing.
Source
Thrown at core/src/main/java/io/grpc/internal/JsonUtil.java:139
String.format("value '%s' for key '%s' in '%s' is not a number", value, key, obj));
}
/**
* Gets a number from an object for the given key, casted to an integer. If the key is not
* present, this returns null. If the value does not represent an integer, throws an exception.
*/
@Nullable
public static Integer getNumberAsInteger(Map<String, ?> obj, String key) {
assert key != null;
if (!obj.containsKey(key)) {
return null;
}
Object value = obj.get(key);
if (value instanceof Double) {
Double d = (Double) value;
int i = d.intValue();
if (i != d) {
throw new ClassCastException("Number expected to be integer: " + d);
}
return i;
}
if (value instanceof String) {
try {
return Integer.parseInt((String) value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("value '%s' for key '%s' is not an integer", value, key));
}
}
throw new IllegalArgumentException(
String.format("value '%s' for key '%s' is not an integer", value, key));
}
/**
* Gets a number from an object for the given key, casted to an long. If the key is not
* present, this returns null. If the value does not represent a long integer, throws anView on GitHub (pinned to 64daddc1f3)
Solutions
- Change the JSON/config value to a whole number that fits in an int (e.g. 1.5 -> 2, 5000000000 -> a value <= 2147483647).
- If the field is genuinely large, switch to a long-based accessor such as getNumberAsLong and widen the consuming field.
- Wrap the call in try/catch for ClassCastException and fail fast with a clear config-validation message at startup.
Example fix
// before
{"timeoutMillis": 4294967296}
// after
{"timeoutMillis": 2147483647} Defensive patterns
Strategy: validation
Validate before calling
Object v = config.get("maxAttempts");
if (v instanceof Double && ((Double) v).intValue() != (Double) v) {
throw new IllegalArgumentException("maxAttempts must be a whole int");
} Type guard
boolean isWholeInt(Object v) {
return v instanceof Double && ((Double) v).intValue() == (Double) v;
} Try / catch
try {
int n = JsonUtil.getNumberAsInteger(config, "maxAttempts");
} catch (ClassCastException e) {
log.error("Bad integer config: " + e.getMessage());
throw new ConfigException(e);
} Prevention
- Never emit fractional numbers for int fields in generated configs.
- Keep int-range values <= 2147483647.
- Run JSON Schema validation over service config before parsing.
When it happens
Trigger: Calling JsonUtil.getNumberAsInteger(map, key) where map.get(key) is a Double whose intValue() differs from the original value, e.g. JSON {"maxAttempts": 1.5} or 1e10 (outside int range).
Common situations: Hand-edited service config or channel argument JSON where someone wrote a decimal like 3.0 is fine but 3.5, or a value exceeding Integer.MAX_VALUE such as 5000000000; configs generated by tools that emit all numbers as doubles.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Number expected to be long:
- Authorization policy should be a JSON object. Found: null
- value '%s' for key '%s' in '%s' is not List
- value '%s' for key '%s' in '%s' is not object
- value '%s' for key '%s' is not a double
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/cc54e5ebf9874424.
Report an issue: GitHub.