From 429865d543a42cb1e893b99f1db2a37e13074cf0 Mon Sep 17 00:00:00 2001 From: daguimu Date: Thu, 26 Mar 2026 10:02:52 +0800 Subject: [PATCH 1/4] fix: Include location info in NumberFormatException from JsonReader JsonReader.nextDouble(), nextInt(), and nextLong() call Double.parseDouble() on peeked strings, but the NumberFormatException thrown by parseDouble() contains no JSON location information (line, column, path), making it difficult to locate the problematic value in the input. Catch the NumberFormatException and rethrow with a message that includes the location string, consistent with other error messages in JsonReader. Fixes #1564 --- .../com/google/gson/stream/JsonReader.java | 22 +++++++++++-- .../google/gson/stream/JsonReaderTest.java | 32 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index e908c79d85..3338de3764 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -1050,7 +1050,13 @@ public double nextDouble() throws IOException { } peeked = PEEKED_BUFFERED; - double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException. + double result; + try { + result = Double.parseDouble(peekedString); + } catch (NumberFormatException e) { + throw new NumberFormatException( + "Expected a double but was " + peekedString + locationString()); + } if (strictness != Strictness.LENIENT && (Double.isNaN(result) || Double.isInfinite(result))) { throw syntaxError("JSON forbids NaN and infinities: " + result); } @@ -1104,7 +1110,12 @@ public long nextLong() throws IOException { } peeked = PEEKED_BUFFERED; - double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException. + double asDouble; + try { + asDouble = Double.parseDouble(peekedString); + } catch (NumberFormatException e) { + throw new NumberFormatException("Expected a long but was " + peekedString + locationString()); + } long result = (long) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'long'. throw new NumberFormatException("Expected a long but was " + peekedString + locationString()); @@ -1347,7 +1358,12 @@ public int nextInt() throws IOException { } peeked = PEEKED_BUFFERED; - double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException. + double asDouble; + try { + asDouble = Double.parseDouble(peekedString); + } catch (NumberFormatException e) { + throw new NumberFormatException("Expected an int but was " + peekedString + locationString()); + } result = (int) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'int'. throw new NumberFormatException("Expected an int but was " + peekedString + locationString()); diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java index 24f15cfb43..d28f318e11 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -701,6 +701,38 @@ public void testLenientQuotedNonFiniteDoubles() throws IOException { reader.endArray(); } + /** Regression test for https://github.com/google/gson/issues/1564 */ + @Test + public void testNextDoubleNumberFormatExceptionContainsLocation() throws IOException { + JsonReader reader = new JsonReader(reader("[\"\" ]")); + reader.setStrictness(Strictness.LENIENT); + reader.beginArray(); + NumberFormatException e = assertThrows(NumberFormatException.class, () -> reader.nextDouble()); + assertThat(e).hasMessageThat().contains("path $[0]"); + } + + /** Regression test for https://github.com/google/gson/issues/1564 */ + @Test + public void testNextIntNumberFormatExceptionContainsLocation() throws IOException { + JsonReader reader = new JsonReader(reader("{\"x\": \"\"}")); + reader.setStrictness(Strictness.LENIENT); + reader.beginObject(); + reader.nextName(); + NumberFormatException e = assertThrows(NumberFormatException.class, () -> reader.nextInt()); + assertThat(e).hasMessageThat().contains("path $.x"); + } + + /** Regression test for https://github.com/google/gson/issues/1564 */ + @Test + public void testNextLongNumberFormatExceptionContainsLocation() throws IOException { + JsonReader reader = new JsonReader(reader("{\"y\": \"\"}")); + reader.setStrictness(Strictness.LENIENT); + reader.beginObject(); + reader.nextName(); + NumberFormatException e = assertThrows(NumberFormatException.class, () -> reader.nextLong()); + assertThat(e).hasMessageThat().contains("path $.y"); + } + @Test public void testStrictNonFiniteDoublesWithSkipValue() throws IOException { String json = "[NaN]"; From effa6daaada4e5726d63b46f6947877f2a12434d Mon Sep 17 00:00:00 2001 From: daguimu Date: Fri, 24 Apr 2026 00:44:37 +0800 Subject: [PATCH 2/4] Chain the original NumberFormatException as cause Address the ErrorProne UnusedException warning (-Werror on JDK 25 / Build Gson subset CI jobs) introduced when the three number-parsing catch blocks in JsonReader were added: set the caught exception as the cause of the rethrown one so debuggers still see the original parse failure. --- .../com/google/gson/stream/JsonReader.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index 3338de3764..bc57e311cf 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -1054,8 +1054,11 @@ public double nextDouble() throws IOException { try { result = Double.parseDouble(peekedString); } catch (NumberFormatException e) { - throw new NumberFormatException( - "Expected a double but was " + peekedString + locationString()); + NumberFormatException rethrown = + new NumberFormatException( + "Expected a double but was " + peekedString + locationString()); + rethrown.initCause(e); + throw rethrown; } if (strictness != Strictness.LENIENT && (Double.isNaN(result) || Double.isInfinite(result))) { throw syntaxError("JSON forbids NaN and infinities: " + result); @@ -1114,7 +1117,11 @@ public long nextLong() throws IOException { try { asDouble = Double.parseDouble(peekedString); } catch (NumberFormatException e) { - throw new NumberFormatException("Expected a long but was " + peekedString + locationString()); + NumberFormatException rethrown = + new NumberFormatException( + "Expected a long but was " + peekedString + locationString()); + rethrown.initCause(e); + throw rethrown; } long result = (long) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'long'. @@ -1362,7 +1369,11 @@ public int nextInt() throws IOException { try { asDouble = Double.parseDouble(peekedString); } catch (NumberFormatException e) { - throw new NumberFormatException("Expected an int but was " + peekedString + locationString()); + NumberFormatException rethrown = + new NumberFormatException( + "Expected an int but was " + peekedString + locationString()); + rethrown.initCause(e); + throw rethrown; } result = (int) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'int'. From 85663fb4cc1b1aeccef7b0f83e55abba5951dd54 Mon Sep 17 00:00:00 2001 From: daguimu Date: Fri, 24 Apr 2026 00:53:34 +0800 Subject: [PATCH 3/4] Collapse NumberFormatException message onto a single line for spotless --- .../src/main/java/com/google/gson/stream/JsonReader.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index bc57e311cf..eaed974527 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -1055,8 +1055,7 @@ public double nextDouble() throws IOException { result = Double.parseDouble(peekedString); } catch (NumberFormatException e) { NumberFormatException rethrown = - new NumberFormatException( - "Expected a double but was " + peekedString + locationString()); + new NumberFormatException("Expected a double but was " + peekedString + locationString()); rethrown.initCause(e); throw rethrown; } @@ -1118,8 +1117,7 @@ public long nextLong() throws IOException { asDouble = Double.parseDouble(peekedString); } catch (NumberFormatException e) { NumberFormatException rethrown = - new NumberFormatException( - "Expected a long but was " + peekedString + locationString()); + new NumberFormatException("Expected a long but was " + peekedString + locationString()); rethrown.initCause(e); throw rethrown; } @@ -1370,8 +1368,7 @@ public int nextInt() throws IOException { asDouble = Double.parseDouble(peekedString); } catch (NumberFormatException e) { NumberFormatException rethrown = - new NumberFormatException( - "Expected an int but was " + peekedString + locationString()); + new NumberFormatException("Expected an int but was " + peekedString + locationString()); rethrown.initCause(e); throw rethrown; } From 4fc1b6d51f77efb295d75fc6793f1371887544af Mon Sep 17 00:00:00 2001 From: daguimu Date: Fri, 24 Apr 2026 00:57:15 +0800 Subject: [PATCH 4/4] Use 'var unused' for intentionally discarded nextName() in tests --- gson/src/test/java/com/google/gson/stream/JsonReaderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java index d28f318e11..e7916ecf37 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -717,7 +717,7 @@ public void testNextIntNumberFormatExceptionContainsLocation() throws IOExceptio JsonReader reader = new JsonReader(reader("{\"x\": \"\"}")); reader.setStrictness(Strictness.LENIENT); reader.beginObject(); - reader.nextName(); + var unused = reader.nextName(); NumberFormatException e = assertThrows(NumberFormatException.class, () -> reader.nextInt()); assertThat(e).hasMessageThat().contains("path $.x"); } @@ -728,7 +728,7 @@ public void testNextLongNumberFormatExceptionContainsLocation() throws IOExcepti JsonReader reader = new JsonReader(reader("{\"y\": \"\"}")); reader.setStrictness(Strictness.LENIENT); reader.beginObject(); - reader.nextName(); + var unused = reader.nextName(); NumberFormatException e = assertThrows(NumberFormatException.class, () -> reader.nextLong()); assertThat(e).hasMessageThat().contains("path $.y"); }