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 an

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Change the JSON/config value to a whole number that fits in an int (e.g. 1.5 -> 2, 5000000000 -> a value <= 2147483647).
  2. If the field is genuinely large, switch to a long-based accessor such as getNumberAsLong and widen the consuming field.
  3. 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

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


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/cc54e5ebf9874424. Report an issue: GitHub.