From 7a5ea7e0ae8d36d591e3f73b66b91829e314d03f Mon Sep 17 00:00:00 2001 From: ErnestHysa Date: Sun, 31 May 2026 16:56:11 +0100 Subject: [PATCH 1/2] Fix #3008: reject trailing data after top-level null Both Gson.fromJson and JsonParser.parseReader accepted trailing data after a top-level null because the trailing-data check was skipped when the parsed element was null. Changed assertFullConsumption() in Gson.java to always check for END_DOCUMENT (removing the 'obj != null' guard), and changed the check in JsonParser.parseReader() to unconditionally verify END_DOCUMENT (removing the '!element.isJsonNull()' guard). --- gson/src/main/java/com/google/gson/Gson.java | 2 +- gson/src/main/java/com/google/gson/JsonParser.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index a06f290747..5fb67d1c32 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -1195,7 +1195,7 @@ public T fromJson(JsonElement json, TypeToken typeOfT) throws JsonSyntaxE private static void assertFullConsumption(Object obj, JsonReader reader) { try { - if (obj != null && reader.peek() != JsonToken.END_DOCUMENT) { + if (reader.peek() != JsonToken.END_DOCUMENT) { throw new JsonSyntaxException("JSON document was not fully consumed."); } } catch (MalformedJsonException e) { diff --git a/gson/src/main/java/com/google/gson/JsonParser.java b/gson/src/main/java/com/google/gson/JsonParser.java index d56da87a48..b2fafdd8d6 100644 --- a/gson/src/main/java/com/google/gson/JsonParser.java +++ b/gson/src/main/java/com/google/gson/JsonParser.java @@ -108,7 +108,7 @@ public static JsonElement parseReader(Reader reader) throws JsonIOException, Jso try { JsonReader jsonReader = new JsonReader(reader); JsonElement element = parseReader(jsonReader); - if (!element.isJsonNull() && jsonReader.peek() != JsonToken.END_DOCUMENT) { + if (jsonReader.peek() != JsonToken.END_DOCUMENT) { throw new JsonSyntaxException("Did not consume the entire document."); } return element; From abf38fd70766ead777c9f247ed4e6916a6424b46 Mon Sep 17 00:00:00 2001 From: ErnestHysa Date: Sun, 31 May 2026 18:39:40 +0100 Subject: [PATCH 2/2] Fix #2817: Add precision check to JsonTreeReader.nextInt() and nextLong() When a double value is stored in a JsonPrimitive, calling nextInt() or nextLong() should throw NumberFormatException to be consistent with JsonReader behavior, instead of silently truncating the value. This fix adds the same precision validation that JsonReader has: - nextInt() now checks if the double value can be exactly represented as an int, and throws if precision would be lost - nextLong() similarly checks for precision loss Fixes inconsistency where JsonReader fails but JsonTreeReader silently casts the value. --- .../gson/internal/bind/JsonTreeReader.java | 48 ++++++++++++++++++- .../internal/bind/JsonTreeReaderTest.java | 23 +++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java index 759310ea20..9f6c0b4d05 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java @@ -258,7 +258,29 @@ public long nextLong() throws IOException { throw new IllegalStateException( "Expected " + JsonToken.NUMBER + " but was " + token + locationString()); } - long result = ((JsonPrimitive) peekStack()).getAsLong(); + Object o = peekStack(); + long result; + if (o instanceof JsonPrimitive) { + JsonPrimitive primitive = (JsonPrimitive) o; + if (primitive.isNumber()) { + Number number = primitive.getAsNumber(); + if (number instanceof Long || number instanceof Integer) { + result = number.longValue(); + } else { + // For other number types (e.g., Double, BigDecimal), check for loss of precision + double doubleValue = number.doubleValue(); + result = (long) doubleValue; + if (result != doubleValue) { + throw new NumberFormatException( + "Expected a long but was " + number + locationString()); + } + } + } else { + result = Long.parseLong(primitive.getAsString()); + } + } else { + throw new IllegalStateException("Unexpected token: " + o); + } popStack(); if (stackSize > 0) { pathIndices[stackSize - 1]++; @@ -273,7 +295,29 @@ public int nextInt() throws IOException { throw new IllegalStateException( "Expected " + JsonToken.NUMBER + " but was " + token + locationString()); } - int result = ((JsonPrimitive) peekStack()).getAsInt(); + Object o = peekStack(); + int result; + if (o instanceof JsonPrimitive) { + JsonPrimitive primitive = (JsonPrimitive) o; + if (primitive.isNumber()) { + Number number = primitive.getAsNumber(); + if (number instanceof Integer) { + result = number.intValue(); + } else { + // For other number types (e.g., Long, Double, BigDecimal), check for loss of precision + double doubleValue = number.doubleValue(); + result = (int) doubleValue; + if (result != doubleValue) { + throw new NumberFormatException( + "Expected an int but was " + number + locationString()); + } + } + } else { + result = Integer.parseInt(primitive.getAsString()); + } + } else { + throw new IllegalStateException("Unexpected token: " + o); + } popStack(); if (stackSize > 0) { pathIndices[stackSize - 1]++; diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java index 42d4649683..f973eb8669 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java @@ -199,4 +199,27 @@ public void testOverrides() { "getNestingLimit()"); MoreAsserts.assertOverridesMethods(JsonReader.class, JsonTreeReader.class, ignoredMethods); } + + // Tests for issue #2817: Inconsistency JsonReader vs JsonTreeReader about double precision loss + @Test + public void testNextIntThrowsForDouble() throws IOException { + JsonObject json = new JsonObject(); + json.addProperty("value", 42.123); + JsonTreeReader reader = new JsonTreeReader(json); + reader.beginObject(); + reader.nextName(); + NumberFormatException e = assertThrows(NumberFormatException.class, reader::nextInt); + assertThat(e).hasMessageThat().startsWith("Expected an int but was"); + } + + @Test + public void testNextLongThrowsForDouble() throws IOException { + JsonObject json = new JsonObject(); + json.addProperty("value", 42.123); + JsonTreeReader reader = new JsonTreeReader(json); + reader.beginObject(); + reader.nextName(); + NumberFormatException e = assertThrows(NumberFormatException.class, reader::nextLong); + assertThat(e).hasMessageThat().startsWith("Expected a long but was"); + } }