Skip to content

Commit

Permalink
Support arbitrary Number implementation for Object and Number deseria…
Browse files Browse the repository at this point in the history
…lization (#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 <[email protected]>
  • Loading branch information
lyubomyr-shaydariv and Marcono1234 authored Oct 9, 2021
1 parent 1cc1627 commit fe30b85
Show file tree
Hide file tree
Showing 10 changed files with 575 additions and 41 deletions.
14 changes: 10 additions & 4 deletions gson/src/main/java/com/google/gson/Gson.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -188,7 +191,7 @@ public 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,
Expand All @@ -198,7 +201,8 @@ public 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;
Expand All @@ -216,12 +220,14 @@ public 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);
Expand All @@ -241,7 +247,7 @@ public 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)));
Expand Down
28 changes: 27 additions & 1 deletion gson/src/main/java/com/google/gson/GsonBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -326,6 +328,30 @@ public GsonBuilder setFieldNamingStrategy(FieldNamingStrategy fieldNamingStrateg
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.
Expand Down Expand Up @@ -600,7 +626,7 @@ public Gson create() {
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,
Expand Down
99 changes: 99 additions & 0 deletions gson/src/main/java/com/google/gson/ToNumberPolicy.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

}
71 changes: 71 additions & 0 deletions gson/src/main/java/com/google/gson/ToNumberStrategy.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit fe30b85

Please sign in to comment.