From cbf05d2948b62bdaa6c80267c9b441820ff9954e Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 25 May 2026 13:59:21 +0000 Subject: [PATCH 1/2] core: support Integer and Long in service configuration parsing Previously, ManagedChannelImplBuilder and JsonUtil only supported Double or String for numeric values in service configurations. This caused IllegalArgumentException when using service configurations produced by JSON parsers like Jackson that represent non-decimal numbers as Integer or Long. This change: - Updates ManagedChannelImplBuilder to accept any Number type. - Enhances JsonUtil to robustly handle numeric conversions while maintaining necessary integral checks. - Adds ServiceConfigNumericTypeTest to verify support for Integer, Double, and Long types. Fixes #12815 --- .../main/java/io/grpc/internal/JsonUtil.java | 63 ++++++++++++----- .../internal/ManagedChannelImplBuilder.java | 4 +- .../java/io/grpc/internal/JsonUtilTest.java | 68 +++++++++++++++++++ .../ManagedChannelImplBuilderTest.java | 34 +++++++++- 4 files changed, 150 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/JsonUtil.java b/core/src/main/java/io/grpc/internal/JsonUtil.java index 6c9274702b6..f633c0910cc 100644 --- a/core/src/main/java/io/grpc/internal/JsonUtil.java +++ b/core/src/main/java/io/grpc/internal/JsonUtil.java @@ -18,6 +18,8 @@ import static com.google.common.math.LongMath.checkedAdd; +import java.math.BigDecimal; +import java.math.BigInteger; import java.text.ParseException; import java.util.List; import java.util.Locale; @@ -106,8 +108,8 @@ public static Double getNumberAsDouble(Map obj, String key) { return null; } Object value = obj.get(key); - if (value instanceof Double) { - return (Double) value; + if (value instanceof Number) { + return ((Number) value).doubleValue(); } if (value instanceof String) { try { @@ -132,8 +134,8 @@ public static Float getNumberAsFloat(Map obj, String key) { return null; } Object value = obj.get(key); - if (value instanceof Float) { - return (Float) value; + if (value instanceof Number) { + return ((Number) value).floatValue(); } if (value instanceof String) { try { @@ -159,13 +161,29 @@ public static Integer getNumberAsInteger(Map obj, String 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); + if (value instanceof Number) { + Number n = (Number) value; + if (n instanceof Integer || n instanceof Short || n instanceof Byte) { + return n.intValue(); } - return i; + try { + if (n instanceof Long) { + return Math.toIntExact(n.longValue()); + } + if (n instanceof BigInteger) { + return ((BigInteger) n).intValueExact(); + } + if (n instanceof BigDecimal) { + return ((BigDecimal) n).intValueExact(); + } + } catch (ArithmeticException e) { + throw new ClassCastException("Number expected to be integer: " + n); + } + double d = n.doubleValue(); + if (Math.rint(d) == d && d >= Integer.MIN_VALUE && d <= Integer.MAX_VALUE) { + return n.intValue(); + } + throw new ClassCastException("Number expected to be integer: " + n); } if (value instanceof String) { try { @@ -190,13 +208,26 @@ public static Long getNumberAsLong(Map obj, String key) { return null; } Object value = obj.get(key); - if (value instanceof Double) { - Double d = (Double) value; - long l = d.longValue(); - if (l != d) { - throw new ClassCastException("Number expected to be long: " + d); + if (value instanceof Number) { + Number n = (Number) value; + if (n instanceof Long || n instanceof Integer || n instanceof Short || n instanceof Byte) { + return n.longValue(); + } + try { + if (n instanceof BigInteger) { + return ((BigInteger) n).longValueExact(); + } + if (n instanceof BigDecimal) { + return ((BigDecimal) n).longValueExact(); + } + } catch (ArithmeticException e) { + throw new ClassCastException("Number expected to be long: " + n); + } + double d = n.doubleValue(); + if (Math.rint(d) == d && d >= (double) Long.MIN_VALUE && d < Math.scalb(1.0, 63)) { + return n.longValue(); } - return l; + throw new ClassCastException("Number expected to be long: " + n); } if (value instanceof String) { try { diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java index 128c929ec0e..5224298984d 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java @@ -582,7 +582,7 @@ public ManagedChannelImplBuilder defaultServiceConfig(@Nullable Map s parsedMap.put(key, checkListEntryTypes((List) value)); } else if (value instanceof String) { parsedMap.put(key, value); - } else if (value instanceof Double) { + } else if (value instanceof Number) { parsedMap.put(key, value); } else if (value instanceof Boolean) { parsedMap.put(key, value); @@ -606,7 +606,7 @@ private static List checkListEntryTypes(List list) { parsedList.add(checkListEntryTypes((List) value)); } else if (value instanceof String) { parsedList.add(value); - } else if (value instanceof Double) { + } else if (value instanceof Number) { parsedList.add(value); } else if (value instanceof Boolean) { parsedList.add(value); diff --git a/core/src/test/java/io/grpc/internal/JsonUtilTest.java b/core/src/test/java/io/grpc/internal/JsonUtilTest.java index 058171814ea..bcae2bfa8bb 100644 --- a/core/src/test/java/io/grpc/internal/JsonUtilTest.java +++ b/core/src/test/java/io/grpc/internal/JsonUtilTest.java @@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -160,6 +162,72 @@ public void getNumber() { } } + @Test + public void getNumber_precision() { + Map map = new HashMap<>(); + long largeLong = (1L << 60) + 1; + map.put("large_long", (double) largeLong); // This is 2^60 + map.put("large_long_actual", largeLong); + map.put("big_int", new BigInteger(String.valueOf(largeLong))); + map.put("big_decimal", new BigDecimal(String.valueOf(largeLong))); + map.put("big_int_too_large", new BigInteger(String.valueOf(largeLong)).multiply(BigInteger.valueOf(100))); + map.put("double_fractional", 1.5D); + map.put("double_nan", Double.NaN); + map.put("double_inf", Double.POSITIVE_INFINITY); + + // Large long represented as double is 2^60, which is a valid long + assertThat(JsonUtil.getNumberAsLong(map, "large_long")).isEqualTo(1L << 60); + + // Large long actual should pass + assertThat(JsonUtil.getNumberAsLong(map, "large_long_actual")).isEqualTo(largeLong); + + // BigInteger and BigDecimal should pass + assertThat(JsonUtil.getNumberAsLong(map, "big_int")).isEqualTo(largeLong); + assertThat(JsonUtil.getNumberAsLong(map, "big_decimal")).isEqualTo(largeLong); + + // Too large BigInteger should fail + try { + JsonUtil.getNumberAsLong(map, "big_int_too_large"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be long:"); + } + + // Integer specific tests + map.put("int_actual", 123); + map.put("long_as_int", 123L); + map.put("long_too_large_for_int", (long) Integer.MAX_VALUE + 1); + + assertThat(JsonUtil.getNumberAsInteger(map, "int_actual")).isEqualTo(123); + assertThat(JsonUtil.getNumberAsInteger(map, "long_as_int")).isEqualTo(123); + try { + JsonUtil.getNumberAsInteger(map, "long_too_large_for_int"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be integer:"); + } + + // Fractional and special doubles + try { + JsonUtil.getNumberAsLong(map, "double_fractional"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be long:"); + } + try { + JsonUtil.getNumberAsLong(map, "double_nan"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be long:"); + } + try { + JsonUtil.getNumberAsLong(map, "double_inf"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be long:"); + } + } + @Test public void getObject_mapExplicitNullValue() { Map mapWithNullValue = Collections.singletonMap("key", null); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java index b0939239477..a34b4debb19 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java @@ -737,7 +737,39 @@ public void defaultServiceConfig_intValue() { Map config = new HashMap<>(); config.put("key", 3); - assertThrows(IllegalArgumentException.class, () -> builder.defaultServiceConfig(config)); + builder.defaultServiceConfig(config); + + assertThat(builder.defaultServiceConfig).containsExactlyEntriesIn(config); + } + + @Test + public void defaultServiceConfig_longValue() { + Map config = new HashMap<>(); + config.put("key", 3L); + + builder.defaultServiceConfig(config); + + assertThat(builder.defaultServiceConfig).containsExactlyEntriesIn(config); + } + + @Test + public void defaultServiceConfig_list_intValue() { + Map config = new HashMap<>(); + config.put("key", Collections.singletonList(3)); + + builder.defaultServiceConfig(config); + + assertThat(builder.defaultServiceConfig).containsExactlyEntriesIn(config); + } + + @Test + public void defaultServiceConfig_list_longValue() { + Map config = new HashMap<>(); + config.put("key", Collections.singletonList(3L)); + + builder.defaultServiceConfig(config); + + assertThat(builder.defaultServiceConfig).containsExactlyEntriesIn(config); } @Test From 26f7b311be9ff9d98c4ccba6b7d73254a8a06c90 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 26 May 2026 15:59:28 +0530 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- core/src/test/java/io/grpc/internal/JsonUtilTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/io/grpc/internal/JsonUtilTest.java b/core/src/test/java/io/grpc/internal/JsonUtilTest.java index bcae2bfa8bb..c4172beab26 100644 --- a/core/src/test/java/io/grpc/internal/JsonUtilTest.java +++ b/core/src/test/java/io/grpc/internal/JsonUtilTest.java @@ -170,7 +170,9 @@ public void getNumber_precision() { map.put("large_long_actual", largeLong); map.put("big_int", new BigInteger(String.valueOf(largeLong))); map.put("big_decimal", new BigDecimal(String.valueOf(largeLong))); - map.put("big_int_too_large", new BigInteger(String.valueOf(largeLong)).multiply(BigInteger.valueOf(100))); + map.put( + "big_int_too_large", + new BigInteger(String.valueOf(largeLong)).multiply(BigInteger.valueOf(100))); map.put("double_fractional", 1.5D); map.put("double_nan", Double.NaN); map.put("double_inf", Double.POSITIVE_INFINITY);