From 1724ced66449c0b43a80e57d65875e6826bb1d9e Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 22 Feb 2025 01:32:30 +0100 Subject: [PATCH] Fix numeric conversion to Long when serializing numbers as JsonElement tree --- gson/src/main/java/com/google/gson/Gson.java | 2 +- .../gson/internal/bind/TypeAdapters.java | 24 ++++-- .../google/gson/functional/JsonTreeTest.java | 80 +++++++++++++++++++ 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index c0bbbfefec..343110dde2 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -496,7 +496,7 @@ public void write(JsonWriter out, Number value) throws IOException { } float floatValue = value.floatValue(); checkValidFloatingPoint(floatValue); - // For backward compatibility don't call `JsonWriter.value(float)` because that method has + // For backward compatibility don't call `JsonWriter#value(float)` because that method has // been newly added and not all custom JsonWriter implementations might override it yet Number floatNumber = value instanceof Float ? value : floatValue; out.value(floatNumber); diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java index 71f98c0034..e747b433f2 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java @@ -210,7 +210,9 @@ public void write(JsonWriter out, Number value) throws IOException { if (value == null) { out.nullValue(); } else { - out.value(value.byteValue()); + // Always wrap as Number; don't use `JsonWriter#value(long)`, which converts number + Number byteNumber = value instanceof Byte ? value : value.byteValue(); + out.value(byteNumber); } } }; @@ -245,7 +247,9 @@ public void write(JsonWriter out, Number value) throws IOException { if (value == null) { out.nullValue(); } else { - out.value(value.shortValue()); + // Always wrap as Number; don't use `JsonWriter#value(long)`, which converts number + Number shortNumber = value instanceof Short ? value : value.shortValue(); + out.value(shortNumber); } } }; @@ -273,7 +277,9 @@ public void write(JsonWriter out, Number value) throws IOException { if (value == null) { out.nullValue(); } else { - out.value(value.intValue()); + // Always wrap as Number; don't use `JsonWriter#value(long)`, which converts number + Number intNumber = value instanceof Integer ? value : value.intValue(); + out.value(intNumber); } } }; @@ -368,7 +374,10 @@ public Number read(JsonReader in) throws IOException { public void write(JsonWriter out, Number value) throws IOException { if (value == null) { out.nullValue(); + } else if (value instanceof Long) { + out.value(value); } else { + // Only perform unwrapping if numeric conversion is necessary out.value(value.longValue()); } } @@ -389,11 +398,13 @@ public Number read(JsonReader in) throws IOException { public void write(JsonWriter out, Number value) throws IOException { if (value == null) { out.nullValue(); + } else if (value instanceof Float) { + out.value(value); } else { - // For backward compatibility don't call `JsonWriter.value(float)` because that method + // For backward compatibility don't call `JsonWriter#value(float)` because that method // has been newly added and not all custom JsonWriter implementations might override // it yet - Number floatNumber = value instanceof Float ? value : value.floatValue(); + Number floatNumber = value.floatValue(); out.value(floatNumber); } } @@ -414,7 +425,10 @@ public Number read(JsonReader in) throws IOException { public void write(JsonWriter out, Number value) throws IOException { if (value == null) { out.nullValue(); + } else if (value instanceof Double) { + out.value(value); } else { + // Only perform unwrapping if numeric conversion is necessary out.value(value.doubleValue()); } } diff --git a/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java b/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java index 82f8aae88c..65f8044205 100644 --- a/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java +++ b/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java @@ -19,13 +19,21 @@ 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.JsonObject; import com.google.gson.JsonPrimitive; import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.internal.LazilyParsedNumber; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.junit.Before; import org.junit.Test; @@ -88,6 +96,68 @@ public void testJsonTreeNull() { assertThat(jsonElement.has("stringValue")).isFalse(); } + @Test + public void testToJsonTreeNumbers() { + List numbers = + List.of( + (byte) 1, + (short) 2, + 3, + 4L, + 5f, + 6d, + BigInteger.valueOf(7), + new BigDecimal(8), + new LazilyParsedNumber("9")); + + Gson gsonSpecialFloats = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + + for (Number number : numbers) { + JsonElement json = gson.toJsonTree(number); + assertIsNumber(json, number); + + json = gsonSpecialFloats.toJsonTree(number); + assertIsNumber(json, number); + } + } + + /** + * Tests {@link Gson#toJsonTree(Object)} with {@link AtomicInteger} and {@link AtomicLong}, which + * should be serialized as non-atomic (and non-mutable) numbers. + */ + @Test + public void testToJsonTreeAtomicNumbers() { + JsonElement json = gson.toJsonTree(new AtomicInteger(1)); + // Current implementation converts int to long, because there is only `JsonWriter#value(long)` + assertIsNumber(json, 1L); + + json = gson.toJsonTree(new AtomicLong(1)); + assertIsNumber(json, 1L); + } + + /** Tests numeric conversion when using {@link Gson#toJsonTree(Object, Type)}. */ + @Test + public void testToJsonTreeNumberConversion() { + JsonElement json = gson.toJsonTree(1.5f, int.class); + assertIsNumber(json, 1); + + json = gson.toJsonTree(500, byte.class); + assertIsNumber(json, (byte) -12); + + json = gson.toJsonTree(1, float.class); + assertIsNumber(json, 1f); + + json = gson.toJsonTree(1, double.class); + assertIsNumber(json, 1d); + + Gson gsonSpecialFloats = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + json = gsonSpecialFloats.toJsonTree(1, float.class); + assertIsNumber(json, 1f); + + json = gsonSpecialFloats.toJsonTree(1, double.class); + assertIsNumber(json, 1d); + } + private static void assertContains(JsonObject json, JsonPrimitive child) { for (Map.Entry entry : json.entrySet()) { JsonElement node = entry.getValue(); @@ -100,6 +170,16 @@ private static void assertContains(JsonObject json, JsonPrimitive child) { throw new AssertionError("Does not contain " + child); } + private static void assertIsNumber(JsonElement json, Number expectedNumber) { + assertThat(json).isInstanceOf(JsonPrimitive.class); + JsonPrimitive jsonPrimitive = (JsonPrimitive) json; + assertThat(jsonPrimitive.isNumber()).isTrue(); + Number number = jsonPrimitive.getAsNumber(); + assertThat(number).isEqualTo(expectedNumber); + // Explicitly check class because Truth's `isEqualTo` is lenient for numeric values + assertThat(number.getClass()).isEqualTo(expectedNumber.getClass()); + } + private static class SubTypeOfBagOfPrimitives extends BagOfPrimitives { @SuppressWarnings("unused") float f = 1.2F;