From fe30b85224316cabf19f5dd3223843437c297802 Mon Sep 17 00:00:00 2001 From: Lyubomyr Shaydariv Date: Sat, 9 Oct 2021 03:09:43 +0300 Subject: [PATCH] Support arbitrary Number implementation for Object and Number deserialization (#1290) * Object and Number type adapters number deserialization can be configured * Change wording of ToNumberStrategy documentation * Use inline links in doc sparingly If the element has already been linked before, don't create a link for every subsequent occurrence. See also (slightly dated) https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html#inlinelinks * Link to default to-number policies in ToNumberStrategy doc * Reduce code duplication for deserializing Number * Hide default factory constants of NumberTypeAdapter and ObjectTypeAdapter This encapsulates the logic a little bit better. Additionally refactored factory created by NumberTypeAdapter to only create TypeAdapter once and then have factory reuse that adapter for better performance. Co-authored-by: Marcono1234 --- gson/src/main/java/com/google/gson/Gson.java | 14 +- .../java/com/google/gson/GsonBuilder.java | 28 +++- .../java/com/google/gson/ToNumberPolicy.java | 99 +++++++++++++ .../com/google/gson/ToNumberStrategy.java | 71 ++++++++++ .../gson/internal/bind/NumberTypeAdapter.java | 82 +++++++++++ .../gson/internal/bind/ObjectTypeAdapter.java | 41 ++++-- .../gson/internal/bind/TypeAdapters.java | 23 --- .../test/java/com/google/gson/GsonTest.java | 9 +- .../com/google/gson/ToNumberPolicyTest.java | 115 +++++++++++++++ .../ToNumberPolicyFunctionalTest.java | 134 ++++++++++++++++++ 10 files changed, 575 insertions(+), 41 deletions(-) create mode 100644 gson/src/main/java/com/google/gson/ToNumberPolicy.java create mode 100644 gson/src/main/java/com/google/gson/ToNumberStrategy.java create mode 100644 gson/src/main/java/com/google/gson/internal/bind/NumberTypeAdapter.java create mode 100644 gson/src/test/java/com/google/gson/ToNumberPolicyTest.java create mode 100644 gson/src/test/java/com/google/gson/functional/ToNumberPolicyFunctionalTest.java diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index a7938c84..1511bbb1 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -47,6 +47,7 @@ import com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory; import com.google.gson.internal.bind.JsonTreeReader; import com.google.gson.internal.bind.JsonTreeWriter; import com.google.gson.internal.bind.MapTypeAdapterFactory; +import com.google.gson.internal.bind.NumberTypeAdapter; import com.google.gson.internal.bind.ObjectTypeAdapter; import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory; import com.google.gson.internal.bind.TypeAdapters; @@ -146,6 +147,8 @@ public final class Gson { final LongSerializationPolicy longSerializationPolicy; final List builderFactories; final List builderHierarchyFactories; + final ToNumberStrategy objectToNumberStrategy; + final ToNumberStrategy numberToNumberStrategy; /** * Constructs a Gson object with default configuration. The default configuration has the @@ -188,7 +191,7 @@ public final class Gson { DEFAULT_PRETTY_PRINT, DEFAULT_LENIENT, DEFAULT_SPECIALIZE_FLOAT_VALUES, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList()); + Collections.emptyList(), ToNumberPolicy.DOUBLE, ToNumberPolicy.LAZILY_PARSED_NUMBER); } Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy, @@ -198,7 +201,8 @@ public final class Gson { LongSerializationPolicy longSerializationPolicy, String datePattern, int dateStyle, int timeStyle, List builderFactories, List builderHierarchyFactories, - List factoriesToBeAdded) { + List factoriesToBeAdded, + ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy) { this.excluder = excluder; this.fieldNamingStrategy = fieldNamingStrategy; this.instanceCreators = instanceCreators; @@ -216,12 +220,14 @@ public final class Gson { this.timeStyle = timeStyle; this.builderFactories = builderFactories; this.builderHierarchyFactories = builderHierarchyFactories; + this.objectToNumberStrategy = objectToNumberStrategy; + this.numberToNumberStrategy = numberToNumberStrategy; List factories = new ArrayList(); // built-in type adapters that cannot be overridden factories.add(TypeAdapters.JSON_ELEMENT_FACTORY); - factories.add(ObjectTypeAdapter.FACTORY); + factories.add(ObjectTypeAdapter.getFactory(objectToNumberStrategy)); // the excluder must precede all adapters that handle user-defined types factories.add(excluder); @@ -241,7 +247,7 @@ public final class Gson { doubleAdapter(serializeSpecialFloatingPointValues))); factories.add(TypeAdapters.newFactory(float.class, Float.class, floatAdapter(serializeSpecialFloatingPointValues))); - factories.add(TypeAdapters.NUMBER_FACTORY); + factories.add(NumberTypeAdapter.getFactory(numberToNumberStrategy)); factories.add(TypeAdapters.ATOMIC_INTEGER_FACTORY); factories.add(TypeAdapters.ATOMIC_BOOLEAN_FACTORY); factories.add(TypeAdapters.newFactory(AtomicLong.class, atomicLongAdapter(longAdapter))); diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index b2fd74ed..1874e7de 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -95,6 +95,8 @@ public final class GsonBuilder { private boolean prettyPrinting = DEFAULT_PRETTY_PRINT; private boolean generateNonExecutableJson = DEFAULT_JSON_NON_EXECUTABLE; private boolean lenient = DEFAULT_LENIENT; + private ToNumberStrategy objectToNumberStrategy = ToNumberPolicy.DOUBLE; + private ToNumberStrategy numberToNumberStrategy = ToNumberPolicy.LAZILY_PARSED_NUMBER; /** * Creates a GsonBuilder instance that can be used to build Gson with various configuration @@ -326,6 +328,30 @@ public final class GsonBuilder { return this; } + /** + * Configures Gson to apply a specific number strategy during deserialization of {@link Object}. + * + * @param objectToNumberStrategy the actual object-to-number strategy + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @see ToNumberPolicy#DOUBLE The default object-to-number strategy + */ + public GsonBuilder setObjectToNumberStrategy(ToNumberStrategy objectToNumberStrategy) { + this.objectToNumberStrategy = objectToNumberStrategy; + return this; + } + + /** + * Configures Gson to apply a specific number strategy during deserialization of {@link Number}. + * + * @param numberToNumberStrategy the actual number-to-number strategy + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @see ToNumberPolicy#LAZILY_PARSED_NUMBER The default number-to-number strategy + */ + public GsonBuilder setNumberToNumberStrategy(ToNumberStrategy numberToNumberStrategy) { + this.numberToNumberStrategy = numberToNumberStrategy; + return this; + } + /** * Configures Gson to apply a set of exclusion strategies during both serialization and * deserialization. Each of the {@code strategies} will be applied as a disjunction rule. @@ -600,7 +626,7 @@ public final class GsonBuilder { generateNonExecutableJson, escapeHtmlChars, prettyPrinting, lenient, serializeSpecialFloatingPointValues, longSerializationPolicy, datePattern, dateStyle, timeStyle, - this.factories, this.hierarchyFactories, factories); + this.factories, this.hierarchyFactories, factories, objectToNumberStrategy, numberToNumberStrategy); } private void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle, diff --git a/gson/src/main/java/com/google/gson/ToNumberPolicy.java b/gson/src/main/java/com/google/gson/ToNumberPolicy.java new file mode 100644 index 00000000..1c6f349d --- /dev/null +++ b/gson/src/main/java/com/google/gson/ToNumberPolicy.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson; + +import java.io.IOException; +import java.math.BigDecimal; + +import com.google.gson.internal.LazilyParsedNumber; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.MalformedJsonException; + +/** + * An enumeration that defines two standard number reading strategies and a couple of + * strategies to overcome some historical Gson limitations while deserializing numbers as + * {@link Object} and {@link Number}. + * + * @see ToNumberStrategy + */ +public enum ToNumberPolicy implements ToNumberStrategy { + + /** + * Using this policy will ensure that numbers will be read as {@link Double} values. + * This is the default strategy used during deserialization of numbers as {@link Object}. + */ + DOUBLE { + @Override public Double readNumber(JsonReader in) throws IOException { + return in.nextDouble(); + } + }, + + /** + * Using this policy will ensure that numbers will be read as a lazily parsed number backed + * by a string. This is the default strategy used during deserialization of numbers as + * {@link Number}. + */ + LAZILY_PARSED_NUMBER { + @Override public Number readNumber(JsonReader in) throws IOException { + return new LazilyParsedNumber(in.nextString()); + } + }, + + /** + * Using this policy will ensure that numbers will be read as {@link Long} or {@link Double} + * values depending on how JSON numbers are represented: {@code Long} if the JSON number can + * be parsed as a {@code Long} value, or otherwise {@code Double} if it can be parsed as a + * {@code Double} value. If the parsed double-precision number results in a positive or negative + * infinity ({@link Double#isInfinite()}) or a NaN ({@link Double#isNaN()}) value and the + * {@code JsonReader} is not {@link JsonReader#isLenient() lenient}, a {@link MalformedJsonException} + * is thrown. + */ + LONG_OR_DOUBLE { + @Override public Number readNumber(JsonReader in) throws IOException, JsonParseException { + String value = in.nextString(); + try { + return Long.parseLong(value); + } catch (NumberFormatException longE) { + try { + Double d = Double.valueOf(value); + if ((d.isInfinite() || d.isNaN()) && !in.isLenient()) { + throw new MalformedJsonException("JSON forbids NaN and infinities: " + d + in); + } + return d; + } catch (NumberFormatException doubleE) { + throw new JsonParseException("Cannot parse " + value, doubleE); + } + } + } + }, + + /** + * Using this policy will ensure that numbers will be read as numbers of arbitrary length + * using {@link BigDecimal}. + */ + BIG_DECIMAL { + @Override public BigDecimal readNumber(JsonReader in) throws IOException { + String value = in.nextString(); + try { + return new BigDecimal(value); + } catch (NumberFormatException e) { + throw new JsonParseException("Cannot parse " + value, e); + } + } + } + +} diff --git a/gson/src/main/java/com/google/gson/ToNumberStrategy.java b/gson/src/main/java/com/google/gson/ToNumberStrategy.java new file mode 100644 index 00000000..db42a4ef --- /dev/null +++ b/gson/src/main/java/com/google/gson/ToNumberStrategy.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson; + +import java.io.IOException; + +import com.google.gson.stream.JsonReader; + +/** + * A strategy that is used to control how numbers should be deserialized for {@link Object} and {@link Number} + * when a concrete type of the deserialized number is unknown in advance. By default, Gson uses the following + * deserialization strategies: + * + *
    + *
  • {@link Double} values are returned for JSON numbers if the deserialization type is declared as + * {@code Object}, see {@link ToNumberPolicy#DOUBLE};
  • + *
  • Lazily parsed number values are returned if the deserialization type is declared as {@code Number}, + * see {@link ToNumberPolicy#LAZILY_PARSED_NUMBER}.
  • + *
+ * + *

For historical reasons, Gson does not support deserialization of arbitrary-length numbers for + * {@code Object} and {@code Number} by default, potentially causing precision loss. However, + * RFC 8259 permits this: + * + *

+ *   This specification allows implementations to set limits on the range
+ *   and precision of numbers accepted.  Since software that implements
+ *   IEEE 754 binary64 (double precision) numbers [IEEE754] is generally
+ *   available and widely used, good interoperability can be achieved by
+ *   implementations that expect no more precision or range than these
+ *   provide, in the sense that implementations will approximate JSON
+ *   numbers within the expected precision.  A JSON number such as 1E400
+ *   or 3.141592653589793238462643383279 may indicate potential
+ *   interoperability problems, since it suggests that the software that
+ *   created it expects receiving software to have greater capabilities
+ *   for numeric magnitude and precision than is widely available.
+ * 
+ * + *

To overcome the precision loss, use for example {@link ToNumberPolicy#LONG_OR_DOUBLE} or + * {@link ToNumberPolicy#BIG_DECIMAL}.

+ * + * @see ToNumberPolicy + * @see GsonBuilder#setObjectToNumberStrategy(ToNumberStrategy) + * @see GsonBuilder#setNumberToNumberStrategy(ToNumberStrategy) + */ +public interface ToNumberStrategy { + + /** + * Reads a number from the given JSON reader. A strategy is supposed to read a single value from the + * reader, and the read value is guaranteed never to be {@code null}. + * + * @param in JSON reader to read a number from + * @return number read from the JSON reader. + * @throws IOException + */ + public Number readNumber(JsonReader in) throws IOException; +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/NumberTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/NumberTypeAdapter.java new file mode 100644 index 00000000..f5efff28 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/NumberTypeAdapter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.ToNumberStrategy; +import com.google.gson.ToNumberPolicy; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * Type adapter for {@link Number}. + */ +public final class NumberTypeAdapter extends TypeAdapter { + /** + * Gson default factory using {@link ToNumberPolicy#LAZILY_PARSED_NUMBER}. + */ + private static final TypeAdapterFactory LAZILY_PARSED_NUMBER_FACTORY = newFactory(ToNumberPolicy.LAZILY_PARSED_NUMBER); + + private final ToNumberStrategy toNumberStrategy; + + private NumberTypeAdapter(ToNumberStrategy toNumberStrategy) { + this.toNumberStrategy = toNumberStrategy; + } + + private static TypeAdapterFactory newFactory(ToNumberStrategy toNumberStrategy) { + final NumberTypeAdapter adapter = new NumberTypeAdapter(toNumberStrategy); + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") + @Override public TypeAdapter create(Gson gson, TypeToken type) { + return type.getRawType() == Number.class ? (TypeAdapter) adapter : null; + } + }; + } + + public static TypeAdapterFactory getFactory(ToNumberStrategy toNumberStrategy) { + if (toNumberStrategy == ToNumberPolicy.LAZILY_PARSED_NUMBER) { + return LAZILY_PARSED_NUMBER_FACTORY; + } else { + return newFactory(toNumberStrategy); + } + } + + @Override public Number read(JsonReader in) throws IOException { + JsonToken jsonToken = in.peek(); + switch (jsonToken) { + case NULL: + in.nextNull(); + return null; + case NUMBER: + case STRING: + return toNumberStrategy.readNumber(in); + default: + throw new JsonSyntaxException("Expecting number, got: " + jsonToken); + } + } + + @Override public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java index ec42e048..b50f61e1 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java @@ -17,6 +17,8 @@ package com.google.gson.internal.bind; import com.google.gson.Gson; +import com.google.gson.ToNumberStrategy; +import com.google.gson.ToNumberPolicy; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.internal.LinkedTreeMap; @@ -35,20 +37,37 @@ import java.util.Map; * serialization and a primitive/Map/List on deserialization. */ public final class ObjectTypeAdapter extends TypeAdapter { - public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { - @SuppressWarnings("unchecked") - @Override public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() == Object.class) { - return (TypeAdapter) new ObjectTypeAdapter(gson); - } - return null; - } - }; + /** + * Gson default factory using {@link ToNumberPolicy#DOUBLE}. + */ + private static final TypeAdapterFactory DOUBLE_FACTORY = newFactory(ToNumberPolicy.DOUBLE); private final Gson gson; + private final ToNumberStrategy toNumberStrategy; - ObjectTypeAdapter(Gson gson) { + private ObjectTypeAdapter(Gson gson, ToNumberStrategy toNumberStrategy) { this.gson = gson; + this.toNumberStrategy = toNumberStrategy; + } + + private static TypeAdapterFactory newFactory(final ToNumberStrategy toNumberStrategy) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") + @Override public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() == Object.class) { + return (TypeAdapter) new ObjectTypeAdapter(gson, toNumberStrategy); + } + return null; + } + }; + } + + public static TypeAdapterFactory getFactory(ToNumberStrategy toNumberStrategy) { + if (toNumberStrategy == ToNumberPolicy.DOUBLE) { + return DOUBLE_FACTORY; + } else { + return newFactory(toNumberStrategy); + } } @Override public Object read(JsonReader in) throws IOException { @@ -76,7 +95,7 @@ public final class ObjectTypeAdapter extends TypeAdapter { return in.nextString(); case NUMBER: - return in.nextDouble(); + return toNumberStrategy.readNumber(in); case BOOLEAN: return in.nextBoolean(); 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 81dda903..cd5ba2e3 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 @@ -343,29 +343,6 @@ public final class TypeAdapters { } }; - public static final TypeAdapter NUMBER = new TypeAdapter() { - @Override - public Number read(JsonReader in) throws IOException { - JsonToken jsonToken = in.peek(); - switch (jsonToken) { - case NULL: - in.nextNull(); - return null; - case NUMBER: - case STRING: - return new LazilyParsedNumber(in.nextString()); - default: - throw new JsonSyntaxException("Expecting number, got: " + jsonToken); - } - } - @Override - public void write(JsonWriter out, Number value) throws IOException { - out.value(value); - } - }; - - public static final TypeAdapterFactory NUMBER_FACTORY = newFactory(Number.class, NUMBER); - public static final TypeAdapter CHARACTER = new TypeAdapter() { @Override public Character read(JsonReader in) throws IOException { diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index eec2ec91..d537f7ad 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -44,12 +44,16 @@ public final class GsonTest extends TestCase { } }; + private static final ToNumberStrategy CUSTOM_OBJECT_TO_NUMBER_STRATEGY = ToNumberPolicy.DOUBLE; + private static final ToNumberStrategy CUSTOM_NUMBER_TO_NUMBER_STRATEGY = ToNumberPolicy.LAZILY_PARSED_NUMBER; + public void testOverridesDefaultExcluder() { Gson gson = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, new HashMap>(), true, false, true, false, true, true, false, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), - new ArrayList(), new ArrayList()); + new ArrayList(), new ArrayList(), + CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY); assertEquals(CUSTOM_EXCLUDER, gson.excluder()); assertEquals(CUSTOM_FIELD_NAMING_STRATEGY, gson.fieldNamingStrategy()); @@ -62,7 +66,8 @@ public final class GsonTest extends TestCase { new HashMap>(), true, false, true, false, true, true, false, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), - new ArrayList(), new ArrayList()); + new ArrayList(), new ArrayList(), + CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY); Gson clone = original.newBuilder() .registerTypeAdapter(Object.class, new TestTypeAdapter()) diff --git a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java new file mode 100644 index 00000000..d4f77f29 --- /dev/null +++ b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson; + +import java.io.IOException; +import java.io.StringReader; +import java.math.BigDecimal; +import com.google.gson.internal.LazilyParsedNumber; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.MalformedJsonException; +import junit.framework.TestCase; + +public class ToNumberPolicyTest extends TestCase { + public void testDouble() throws IOException { + ToNumberStrategy strategy = ToNumberPolicy.DOUBLE; + assertEquals(10.1, strategy.readNumber(fromString("10.1"))); + assertEquals(3.141592653589793D, strategy.readNumber(fromString("3.141592653589793238462643383279"))); + try { + strategy.readNumber(fromString("1e400")); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testLazilyParsedNumber() throws IOException { + ToNumberStrategy strategy = ToNumberPolicy.LAZILY_PARSED_NUMBER; + assertEquals(new LazilyParsedNumber("10.1"), strategy.readNumber(fromString("10.1"))); + assertEquals(new LazilyParsedNumber("3.141592653589793238462643383279"), strategy.readNumber(fromString("3.141592653589793238462643383279"))); + assertEquals(new LazilyParsedNumber("1e400"), strategy.readNumber(fromString("1e400"))); + } + + public void testLongOrDouble() throws IOException { + ToNumberStrategy strategy = ToNumberPolicy.LONG_OR_DOUBLE; + assertEquals(10L, strategy.readNumber(fromString("10"))); + assertEquals(10.1, strategy.readNumber(fromString("10.1"))); + assertEquals(3.141592653589793D, strategy.readNumber(fromString("3.141592653589793238462643383279"))); + try { + strategy.readNumber(fromString("1e400")); + fail(); + } catch (MalformedJsonException expected) { + } + assertEquals(Double.NaN, strategy.readNumber(fromStringLenient("NaN"))); + assertEquals(Double.POSITIVE_INFINITY, strategy.readNumber(fromStringLenient("Infinity"))); + assertEquals(Double.NEGATIVE_INFINITY, strategy.readNumber(fromStringLenient("-Infinity"))); + try { + strategy.readNumber(fromString("NaN")); + fail(); + } catch (MalformedJsonException expected) { + } + try { + strategy.readNumber(fromString("Infinity")); + fail(); + } catch (MalformedJsonException expected) { + } + try { + strategy.readNumber(fromString("-Infinity")); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testBigDecimal() throws IOException { + ToNumberStrategy strategy = ToNumberPolicy.BIG_DECIMAL; + assertEquals(new BigDecimal("10.1"), strategy.readNumber(fromString("10.1"))); + assertEquals(new BigDecimal("3.141592653589793238462643383279"), strategy.readNumber(fromString("3.141592653589793238462643383279"))); + assertEquals(new BigDecimal("1e400"), strategy.readNumber(fromString("1e400"))); + } + + public void testNullsAreNeverExpected() throws IOException { + try { + ToNumberPolicy.DOUBLE.readNumber(fromString("null")); + fail(); + } catch (IllegalStateException expected) { + } + try { + ToNumberPolicy.LAZILY_PARSED_NUMBER.readNumber(fromString("null")); + fail(); + } catch (IllegalStateException expected) { + } + try { + ToNumberPolicy.LONG_OR_DOUBLE.readNumber(fromString("null")); + fail(); + } catch (IllegalStateException expected) { + } + try { + ToNumberPolicy.BIG_DECIMAL.readNumber(fromString("null")); + fail(); + } catch (IllegalStateException expected) { + } + } + + private static JsonReader fromString(String json) { + return new JsonReader(new StringReader(json)); + } + + private static JsonReader fromStringLenient(String json) { + JsonReader jsonReader = fromString(json); + jsonReader.setLenient(true); + return jsonReader; + } +} diff --git a/gson/src/test/java/com/google/gson/functional/ToNumberPolicyFunctionalTest.java b/gson/src/test/java/com/google/gson/functional/ToNumberPolicyFunctionalTest.java new file mode 100644 index 00000000..07d99b81 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ToNumberPolicyFunctionalTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson.functional; + +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; +import com.google.gson.ToNumberStrategy; +import com.google.gson.internal.LazilyParsedNumber; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import junit.framework.TestCase; + +public class ToNumberPolicyFunctionalTest extends TestCase { + public void testDefault() { + Gson gson = new Gson(); + assertEquals(null, gson.fromJson("null", Object.class)); + assertEquals(10D, gson.fromJson("10", Object.class)); + assertEquals(null, gson.fromJson("null", Number.class)); + assertEquals(new LazilyParsedNumber("10"), gson.fromJson("10", Number.class)); + } + + public void testAsDoubles() { + Gson gson = new GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.DOUBLE) + .setNumberToNumberStrategy(ToNumberPolicy.DOUBLE) + .create(); + assertEquals(null, gson.fromJson("null", Object.class)); + assertEquals(10.0, gson.fromJson("10", Object.class)); + assertEquals(null, gson.fromJson("null", Number.class)); + assertEquals(10.0, gson.fromJson("10", Number.class)); + } + + public void testAsLazilyParsedNumbers() { + Gson gson = new GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.LAZILY_PARSED_NUMBER) + .setNumberToNumberStrategy(ToNumberPolicy.LAZILY_PARSED_NUMBER) + .create(); + assertEquals(null, gson.fromJson("null", Object.class)); + assertEquals(new LazilyParsedNumber("10"), gson.fromJson("10", Object.class)); + assertEquals(null, gson.fromJson("null", Number.class)); + assertEquals(new LazilyParsedNumber("10"), gson.fromJson("10", Number.class)); + } + + public void testAsLongsOrDoubles() { + Gson gson = new GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .create(); + assertEquals(null, gson.fromJson("null", Object.class)); + assertEquals(10L, gson.fromJson("10", Object.class)); + assertEquals(10.0, gson.fromJson("10.0", Object.class)); + assertEquals(null, gson.fromJson("null", Number.class)); + assertEquals(10L, gson.fromJson("10", Number.class)); + assertEquals(10.0, gson.fromJson("10.0", Number.class)); + } + + public void testAsBigDecimals() { + Gson gson = new GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.BIG_DECIMAL) + .setNumberToNumberStrategy(ToNumberPolicy.BIG_DECIMAL) + .create(); + assertEquals(null, gson.fromJson("null", Object.class)); + assertEquals(new BigDecimal("10"), gson.fromJson("10", Object.class)); + assertEquals(new BigDecimal("10.0"), gson.fromJson("10.0", Object.class)); + assertEquals(null, gson.fromJson("null", Number.class)); + assertEquals(new BigDecimal("10"), gson.fromJson("10", Number.class)); + assertEquals(new BigDecimal("10.0"), gson.fromJson("10.0", Number.class)); + assertEquals(new BigDecimal("3.141592653589793238462643383279"), gson.fromJson("3.141592653589793238462643383279", BigDecimal.class)); + assertEquals(new BigDecimal("1e400"), gson.fromJson("1e400", BigDecimal.class)); + } + + public void testAsListOfLongsOrDoubles() { + Gson gson = new GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .create(); + List expected = new LinkedList(); + expected.add(null); + expected.add(10L); + expected.add(10.0); + Type objectCollectionType = new TypeToken>() { }.getType(); + Collection objects = gson.fromJson("[null,10,10.0]", objectCollectionType); + assertEquals(expected, objects); + Type numberCollectionType = new TypeToken>() { }.getType(); + Collection numbers = gson.fromJson("[null,10,10.0]", numberCollectionType); + assertEquals(expected, numbers); + } + + public void testCustomStrategiesCannotAffectConcreteDeclaredNumbers() { + ToNumberStrategy fail = new ToNumberStrategy() { + @Override + public Byte readNumber(JsonReader in) { + throw new UnsupportedOperationException(); + } + }; + Gson gson = new GsonBuilder() + .setObjectToNumberStrategy(fail) + .setNumberToNumberStrategy(fail) + .create(); + List numbers = gson.fromJson("[null, 10, 20, 30]", new TypeToken>() {}.getType()); + assertEquals(Arrays.asList(null, (byte) 10, (byte) 20, (byte) 30), numbers); + try { + gson.fromJson("[null, 10, 20, 30]", new TypeToken>() {}.getType()); + fail(); + } catch (UnsupportedOperationException ex) { + } + try { + gson.fromJson("[null, 10, 20, 30]", new TypeToken>() {}.getType()); + fail(); + } catch (UnsupportedOperationException ex) { + } + } +}