From 8ef68e2a58937fe9a43400939e8762474341fad0 Mon Sep 17 00:00:00 2001 From: andrewstellman Date: Sat, 6 Jun 2026 01:45:20 -0400 Subject: [PATCH 1/3] test: add failing test for JsonTreeWriter BigDecimal overflow JsonTreeWriter.value(Number) rejects a finite BigDecimal value that JsonWriter.value(Number) accepts in STRICT mode, because the tree writer's finiteness check uses doubleValue() which can overflow for large finite BigDecimal/BigInteger values. This commit adds the failing regression test that demonstrates the divergence between the streaming and tree writer surfaces. --- .../JsonTreeWriterFiniteBigDecimalTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 gson/src/test/java/com/google/gson/regression/JsonTreeWriterFiniteBigDecimalTest.java diff --git a/gson/src/test/java/com/google/gson/regression/JsonTreeWriterFiniteBigDecimalTest.java b/gson/src/test/java/com/google/gson/regression/JsonTreeWriterFiniteBigDecimalTest.java new file mode 100644 index 0000000000..7cd15e23ae --- /dev/null +++ b/gson/src/test/java/com/google/gson/regression/JsonTreeWriterFiniteBigDecimalTest.java @@ -0,0 +1,36 @@ +// Generated by Quality Playbook v1.5.8 — https://github.com/andrewstellman/quality-playbook +// Author: Andrew Stellman · Date: 2026-06-04 · Project: gson +package com.google.gson.regression; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.Strictness; +import java.math.BigDecimal; +import org.junit.Test; + +/** + * Regression test: toJson and toJsonTree must agree on a finite BigDecimal in STRICT mode. + */ +public class JsonTreeWriterFiniteBigDecimalTest { + @Test + public void treeWriterAcceptsFiniteBigDecimalLikeStreamWriter() { + Gson gson = new GsonBuilder().setStrictness(Strictness.STRICT).create(); + BigDecimal finiteHuge = new BigDecimal("1E400"); + + // The bug's defining contract: toJson and toJsonTree must agree on a finite BigDecimal. + // Pre-fix, toJsonTree threw IllegalArgumentException because JsonTreeWriter.value(Number) + // routed through doubleValue() which overflows to Infinity for 1E400. Pin both the + // Number.class and BigDecimal.class type-token dispatch paths so a future adapter-routing + // refactor cannot green the Number-typed path without also fixing BigDecimal-typed dispatch. + String stringViaNumber = gson.toJson(finiteHuge, Number.class); + JsonElement treeViaNumber = gson.toJsonTree(finiteHuge, Number.class); + assertThat(treeViaNumber.toString()).isEqualTo(stringViaNumber); + + String stringViaBigDecimal = gson.toJson(finiteHuge, BigDecimal.class); + JsonElement treeViaBigDecimal = gson.toJsonTree(finiteHuge, BigDecimal.class); + assertThat(treeViaBigDecimal.toString()).isEqualTo(stringViaBigDecimal); + } +} From 16acada0ce85b70fbff38e7d8ff82a41c1cd9607 Mon Sep 17 00:00:00 2001 From: andrewstellman Date: Sat, 6 Jun 2026 01:46:11 -0400 Subject: [PATCH 2/3] fix: JsonTreeWriter accepts finite BigDecimal/BigInteger like JsonWriter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tree writer's finiteness check used doubleValue(), which can overflow to Infinity for finite BigDecimal/BigInteger values like 1E400 — causing toJsonTree to reject inputs that toJson accepts. JsonWriter.value(Number) already special-cases the 'alwaysCreatesValidJsonNumber' classes (BigDecimal, BigInteger, AtomicInteger, AtomicLong) to skip the doubleValue-based oracle. This change mirrors that guard in JsonTreeWriter.value(Number), making the two writer surfaces agree. Existing tests (ToNumberPolicyTest, JsonTreeWriterTest, etc.) continue to pass; the new JsonTreeWriterFiniteBigDecimalTest now passes. --- .../java/com/google/gson/internal/bind/JsonTreeWriter.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java index fda2cf131c..7d18b6053b 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java @@ -25,6 +25,8 @@ import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.io.Writer; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -218,7 +220,10 @@ public JsonWriter value(Number value) throws IOException { return nullValue(); } - if (!isLenient()) { + // BigDecimal/BigInteger are always valid JSON numbers (mirrors + // JsonWriter.alwaysCreatesValidJsonNumber); their doubleValue() can overflow to Infinity + // even though the value is finite, so do not use it as the finiteness oracle for them. + if (!isLenient() && !(value instanceof BigDecimal) && !(value instanceof BigInteger)) { double d = value.doubleValue(); if (Double.isNaN(d) || Double.isInfinite(d)) { throw new IllegalArgumentException("JSON forbids NaN and infinities: " + value); From 2bff7f88b470228bc29416419f0e58e33415b177 Mon Sep 17 00:00:00 2001 From: andrewstellman Date: Sat, 6 Jun 2026 01:50:58 -0400 Subject: [PATCH 3/3] test: add failing test for JsonTreeWriter BigDecimal overflow JsonTreeWriter.value(Number) rejects a finite BigDecimal value that JsonWriter.value(Number) accepts in STRICT mode, because the tree writer's finiteness check uses doubleValue() which can overflow for large finite BigDecimal/BigInteger values. This commit adds the failing regression test that demonstrates the divergence between the streaming and tree writer surfaces. --- .../internal/bind/JsonTreeWriterTest.java | 34 ++++++++++++++++++ .../JsonTreeWriterFiniteBigDecimalTest.java | 36 ------------------- 2 files changed, 34 insertions(+), 36 deletions(-) delete mode 100644 gson/src/test/java/com/google/gson/regression/JsonTreeWriterFiniteBigDecimalTest.java diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java index 3b8ad883e6..4137e2ff9b 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java @@ -27,6 +27,8 @@ import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.io.Writer; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Arrays; import java.util.List; import org.junit.Test; @@ -316,4 +318,36 @@ public void testEndArrayWhenStackTopIsNotArrayThrows() throws IOException { writer.beginObject(); assertThrows(IllegalStateException.class, () -> writer.endArray()); } + + @Test + public void testStrictWriterAcceptsFiniteBigDecimalAboveDoubleMax() throws IOException { + // Pre-fix, JsonTreeWriter.value(Number) routed Number arguments through + // doubleValue() for finiteness checking and rejected BigDecimal("1E400") — + // a finite value whose doubleValue() overflows to Infinity. JsonWriter + // already skips that check for BigDecimal/BigInteger via the + // alwaysCreatesValidJsonNumber whitelist; this test pins that JsonTreeWriter + // now mirrors that behavior in STRICT mode. + JsonTreeWriter writer = new JsonTreeWriter(); + writer.setStrictness(Strictness.STRICT); + writer.beginArray(); + writer.value(new BigDecimal("1E400")); + writer.endArray(); + assertThat(writer.get().toString()).isEqualTo("[1E+400]"); + } + + @Test + public void testStrictWriterAcceptsFiniteBigIntegerAboveDoubleMax() throws IOException { + // Same fix surface as the BigDecimal case above but for BigInteger. + // doubleValue() on a BigInteger > Double.MAX_VALUE overflows to Infinity; + // the alwaysCreatesValidJsonNumber whitelist covers both classes and the + // fix mirrors that. Pin BigInteger separately so a future change to the + // BigDecimal-only branch cannot silently regress BigInteger handling. + JsonTreeWriter writer = new JsonTreeWriter(); + writer.setStrictness(Strictness.STRICT); + BigInteger finiteHuge = BigInteger.TEN.pow(400); + writer.beginArray(); + writer.value(finiteHuge); + writer.endArray(); + assertThat(writer.get().toString()).isEqualTo("[" + finiteHuge + "]"); + } } diff --git a/gson/src/test/java/com/google/gson/regression/JsonTreeWriterFiniteBigDecimalTest.java b/gson/src/test/java/com/google/gson/regression/JsonTreeWriterFiniteBigDecimalTest.java deleted file mode 100644 index 7cd15e23ae..0000000000 --- a/gson/src/test/java/com/google/gson/regression/JsonTreeWriterFiniteBigDecimalTest.java +++ /dev/null @@ -1,36 +0,0 @@ -// Generated by Quality Playbook v1.5.8 — https://github.com/andrewstellman/quality-playbook -// Author: Andrew Stellman · Date: 2026-06-04 · Project: gson -package com.google.gson.regression; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.Strictness; -import java.math.BigDecimal; -import org.junit.Test; - -/** - * Regression test: toJson and toJsonTree must agree on a finite BigDecimal in STRICT mode. - */ -public class JsonTreeWriterFiniteBigDecimalTest { - @Test - public void treeWriterAcceptsFiniteBigDecimalLikeStreamWriter() { - Gson gson = new GsonBuilder().setStrictness(Strictness.STRICT).create(); - BigDecimal finiteHuge = new BigDecimal("1E400"); - - // The bug's defining contract: toJson and toJsonTree must agree on a finite BigDecimal. - // Pre-fix, toJsonTree threw IllegalArgumentException because JsonTreeWriter.value(Number) - // routed through doubleValue() which overflows to Infinity for 1E400. Pin both the - // Number.class and BigDecimal.class type-token dispatch paths so a future adapter-routing - // refactor cannot green the Number-typed path without also fixing BigDecimal-typed dispatch. - String stringViaNumber = gson.toJson(finiteHuge, Number.class); - JsonElement treeViaNumber = gson.toJsonTree(finiteHuge, Number.class); - assertThat(treeViaNumber.toString()).isEqualTo(stringViaNumber); - - String stringViaBigDecimal = gson.toJson(finiteHuge, BigDecimal.class); - JsonElement treeViaBigDecimal = gson.toJsonTree(finiteHuge, BigDecimal.class); - assertThat(treeViaBigDecimal.toString()).isEqualTo(stringViaBigDecimal); - } -}