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 <Marcono1234@users.noreply.github.com>
This commit is contained in:
Lyubomyr Shaydariv 2021-10-09 03:09:43 +03:00 committed by GitHub
parent 1cc1627423
commit fe30b85224
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 575 additions and 41 deletions

View File

@ -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<TypeAdapterFactory> builderFactories;
final List<TypeAdapterFactory> 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.<TypeAdapterFactory>emptyList(), Collections.<TypeAdapterFactory>emptyList(),
Collections.<TypeAdapterFactory>emptyList());
Collections.<TypeAdapterFactory>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<TypeAdapterFactory> builderFactories,
List<TypeAdapterFactory> builderHierarchyFactories,
List<TypeAdapterFactory> factoriesToBeAdded) {
List<TypeAdapterFactory> 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<TypeAdapterFactory> factories = new ArrayList<TypeAdapterFactory>();
// 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)));

View File

@ -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,

View File

@ -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);
}
}
}
}

View File

@ -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:
*
* <ul>
* <li>{@link Double} values are returned for JSON numbers if the deserialization type is declared as
* {@code Object}, see {@link ToNumberPolicy#DOUBLE};</li>
* <li>Lazily parsed number values are returned if the deserialization type is declared as {@code Number},
* see {@link ToNumberPolicy#LAZILY_PARSED_NUMBER}.</li>
* </ul>
*
* <p>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,
* <a href="https://tools.ietf.org/html/rfc8259#section-6">RFC 8259</a> permits this:
*
* <pre>
* 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.
* </pre>
*
* <p>To overcome the precision loss, use for example {@link ToNumberPolicy#LONG_OR_DOUBLE} or
* {@link ToNumberPolicy#BIG_DECIMAL}.</p>
*
* @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;
}

View File

@ -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<Number> {
/**
* 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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
return type.getRawType() == Number.class ? (TypeAdapter<T>) 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);
}
}

View File

@ -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<Object> {
public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
@SuppressWarnings("unchecked")
@Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType() == Object.class) {
return (TypeAdapter<T>) 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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType() == Object.class) {
return (TypeAdapter<T>) 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<Object> {
return in.nextString();
case NUMBER:
return in.nextDouble();
return toNumberStrategy.readNumber(in);
case BOOLEAN:
return in.nextBoolean();

View File

@ -343,29 +343,6 @@ public final class TypeAdapters {
}
};
public static final TypeAdapter<Number> NUMBER = new TypeAdapter<Number>() {
@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> CHARACTER = new TypeAdapter<Character>() {
@Override
public Character read(JsonReader in) throws IOException {

View File

@ -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<Type, InstanceCreator<?>>(), true, false, true, false,
true, true, false, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT,
DateFormat.DEFAULT, new ArrayList<TypeAdapterFactory>(),
new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>());
new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(),
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<Type, InstanceCreator<?>>(), true, false, true, false,
true, true, false, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT,
DateFormat.DEFAULT, new ArrayList<TypeAdapterFactory>(),
new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>());
new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(),
CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY);
Gson clone = original.newBuilder()
.registerTypeAdapter(Object.class, new TestTypeAdapter())

View File

@ -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;
}
}

View File

@ -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<Object> expected = new LinkedList<Object>();
expected.add(null);
expected.add(10L);
expected.add(10.0);
Type objectCollectionType = new TypeToken<Collection<Object>>() { }.getType();
Collection<Object> objects = gson.fromJson("[null,10,10.0]", objectCollectionType);
assertEquals(expected, objects);
Type numberCollectionType = new TypeToken<Collection<Number>>() { }.getType();
Collection<Object> 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<Object> numbers = gson.fromJson("[null, 10, 20, 30]", new TypeToken<List<Byte>>() {}.getType());
assertEquals(Arrays.asList(null, (byte) 10, (byte) 20, (byte) 30), numbers);
try {
gson.fromJson("[null, 10, 20, 30]", new TypeToken<List<Object>>() {}.getType());
fail();
} catch (UnsupportedOperationException ex) {
}
try {
gson.fromJson("[null, 10, 20, 30]", new TypeToken<List<Number>>() {}.getType());
fail();
} catch (UnsupportedOperationException ex) {
}
}
}