Skip to content

Commit fe30b85

Browse files
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>
1 parent 1cc1627 commit fe30b85

File tree

10 files changed

+575
-41
lines changed

10 files changed

+575
-41
lines changed

gson/src/main/java/com/google/gson/Gson.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import com.google.gson.internal.bind.JsonTreeReader;
4848
import com.google.gson.internal.bind.JsonTreeWriter;
4949
import com.google.gson.internal.bind.MapTypeAdapterFactory;
50+
import com.google.gson.internal.bind.NumberTypeAdapter;
5051
import com.google.gson.internal.bind.ObjectTypeAdapter;
5152
import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
5253
import com.google.gson.internal.bind.TypeAdapters;
@@ -146,6 +147,8 @@ public final class Gson {
146147
final LongSerializationPolicy longSerializationPolicy;
147148
final List<TypeAdapterFactory> builderFactories;
148149
final List<TypeAdapterFactory> builderHierarchyFactories;
150+
final ToNumberStrategy objectToNumberStrategy;
151+
final ToNumberStrategy numberToNumberStrategy;
149152

150153
/**
151154
* Constructs a Gson object with default configuration. The default configuration has the
@@ -188,7 +191,7 @@ public Gson() {
188191
DEFAULT_PRETTY_PRINT, DEFAULT_LENIENT, DEFAULT_SPECIALIZE_FLOAT_VALUES,
189192
LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT,
190193
Collections.<TypeAdapterFactory>emptyList(), Collections.<TypeAdapterFactory>emptyList(),
191-
Collections.<TypeAdapterFactory>emptyList());
194+
Collections.<TypeAdapterFactory>emptyList(), ToNumberPolicy.DOUBLE, ToNumberPolicy.LAZILY_PARSED_NUMBER);
192195
}
193196

194197
Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy,
@@ -198,7 +201,8 @@ public Gson() {
198201
LongSerializationPolicy longSerializationPolicy, String datePattern, int dateStyle,
199202
int timeStyle, List<TypeAdapterFactory> builderFactories,
200203
List<TypeAdapterFactory> builderHierarchyFactories,
201-
List<TypeAdapterFactory> factoriesToBeAdded) {
204+
List<TypeAdapterFactory> factoriesToBeAdded,
205+
ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy) {
202206
this.excluder = excluder;
203207
this.fieldNamingStrategy = fieldNamingStrategy;
204208
this.instanceCreators = instanceCreators;
@@ -216,12 +220,14 @@ public Gson() {
216220
this.timeStyle = timeStyle;
217221
this.builderFactories = builderFactories;
218222
this.builderHierarchyFactories = builderHierarchyFactories;
223+
this.objectToNumberStrategy = objectToNumberStrategy;
224+
this.numberToNumberStrategy = numberToNumberStrategy;
219225

220226
List<TypeAdapterFactory> factories = new ArrayList<TypeAdapterFactory>();
221227

222228
// built-in type adapters that cannot be overridden
223229
factories.add(TypeAdapters.JSON_ELEMENT_FACTORY);
224-
factories.add(ObjectTypeAdapter.FACTORY);
230+
factories.add(ObjectTypeAdapter.getFactory(objectToNumberStrategy));
225231

226232
// the excluder must precede all adapters that handle user-defined types
227233
factories.add(excluder);
@@ -241,7 +247,7 @@ public Gson() {
241247
doubleAdapter(serializeSpecialFloatingPointValues)));
242248
factories.add(TypeAdapters.newFactory(float.class, Float.class,
243249
floatAdapter(serializeSpecialFloatingPointValues)));
244-
factories.add(TypeAdapters.NUMBER_FACTORY);
250+
factories.add(NumberTypeAdapter.getFactory(numberToNumberStrategy));
245251
factories.add(TypeAdapters.ATOMIC_INTEGER_FACTORY);
246252
factories.add(TypeAdapters.ATOMIC_BOOLEAN_FACTORY);
247253
factories.add(TypeAdapters.newFactory(AtomicLong.class, atomicLongAdapter(longAdapter)));

gson/src/main/java/com/google/gson/GsonBuilder.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ public final class GsonBuilder {
9595
private boolean prettyPrinting = DEFAULT_PRETTY_PRINT;
9696
private boolean generateNonExecutableJson = DEFAULT_JSON_NON_EXECUTABLE;
9797
private boolean lenient = DEFAULT_LENIENT;
98+
private ToNumberStrategy objectToNumberStrategy = ToNumberPolicy.DOUBLE;
99+
private ToNumberStrategy numberToNumberStrategy = ToNumberPolicy.LAZILY_PARSED_NUMBER;
98100

99101
/**
100102
* Creates a GsonBuilder instance that can be used to build Gson with various configuration
@@ -326,6 +328,30 @@ public GsonBuilder setFieldNamingStrategy(FieldNamingStrategy fieldNamingStrateg
326328
return this;
327329
}
328330

331+
/**
332+
* Configures Gson to apply a specific number strategy during deserialization of {@link Object}.
333+
*
334+
* @param objectToNumberStrategy the actual object-to-number strategy
335+
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
336+
* @see ToNumberPolicy#DOUBLE The default object-to-number strategy
337+
*/
338+
public GsonBuilder setObjectToNumberStrategy(ToNumberStrategy objectToNumberStrategy) {
339+
this.objectToNumberStrategy = objectToNumberStrategy;
340+
return this;
341+
}
342+
343+
/**
344+
* Configures Gson to apply a specific number strategy during deserialization of {@link Number}.
345+
*
346+
* @param numberToNumberStrategy the actual number-to-number strategy
347+
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
348+
* @see ToNumberPolicy#LAZILY_PARSED_NUMBER The default number-to-number strategy
349+
*/
350+
public GsonBuilder setNumberToNumberStrategy(ToNumberStrategy numberToNumberStrategy) {
351+
this.numberToNumberStrategy = numberToNumberStrategy;
352+
return this;
353+
}
354+
329355
/**
330356
* Configures Gson to apply a set of exclusion strategies during both serialization and
331357
* deserialization. Each of the {@code strategies} will be applied as a disjunction rule.
@@ -600,7 +626,7 @@ public Gson create() {
600626
generateNonExecutableJson, escapeHtmlChars, prettyPrinting, lenient,
601627
serializeSpecialFloatingPointValues, longSerializationPolicy,
602628
datePattern, dateStyle, timeStyle,
603-
this.factories, this.hierarchyFactories, factories);
629+
this.factories, this.hierarchyFactories, factories, objectToNumberStrategy, numberToNumberStrategy);
604630
}
605631

606632
private void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle,
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (C) 2021 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.gson;
18+
19+
import java.io.IOException;
20+
import java.math.BigDecimal;
21+
22+
import com.google.gson.internal.LazilyParsedNumber;
23+
import com.google.gson.stream.JsonReader;
24+
import com.google.gson.stream.MalformedJsonException;
25+
26+
/**
27+
* An enumeration that defines two standard number reading strategies and a couple of
28+
* strategies to overcome some historical Gson limitations while deserializing numbers as
29+
* {@link Object} and {@link Number}.
30+
*
31+
* @see ToNumberStrategy
32+
*/
33+
public enum ToNumberPolicy implements ToNumberStrategy {
34+
35+
/**
36+
* Using this policy will ensure that numbers will be read as {@link Double} values.
37+
* This is the default strategy used during deserialization of numbers as {@link Object}.
38+
*/
39+
DOUBLE {
40+
@Override public Double readNumber(JsonReader in) throws IOException {
41+
return in.nextDouble();
42+
}
43+
},
44+
45+
/**
46+
* Using this policy will ensure that numbers will be read as a lazily parsed number backed
47+
* by a string. This is the default strategy used during deserialization of numbers as
48+
* {@link Number}.
49+
*/
50+
LAZILY_PARSED_NUMBER {
51+
@Override public Number readNumber(JsonReader in) throws IOException {
52+
return new LazilyParsedNumber(in.nextString());
53+
}
54+
},
55+
56+
/**
57+
* Using this policy will ensure that numbers will be read as {@link Long} or {@link Double}
58+
* values depending on how JSON numbers are represented: {@code Long} if the JSON number can
59+
* be parsed as a {@code Long} value, or otherwise {@code Double} if it can be parsed as a
60+
* {@code Double} value. If the parsed double-precision number results in a positive or negative
61+
* infinity ({@link Double#isInfinite()}) or a NaN ({@link Double#isNaN()}) value and the
62+
* {@code JsonReader} is not {@link JsonReader#isLenient() lenient}, a {@link MalformedJsonException}
63+
* is thrown.
64+
*/
65+
LONG_OR_DOUBLE {
66+
@Override public Number readNumber(JsonReader in) throws IOException, JsonParseException {
67+
String value = in.nextString();
68+
try {
69+
return Long.parseLong(value);
70+
} catch (NumberFormatException longE) {
71+
try {
72+
Double d = Double.valueOf(value);
73+
if ((d.isInfinite() || d.isNaN()) && !in.isLenient()) {
74+
throw new MalformedJsonException("JSON forbids NaN and infinities: " + d + in);
75+
}
76+
return d;
77+
} catch (NumberFormatException doubleE) {
78+
throw new JsonParseException("Cannot parse " + value, doubleE);
79+
}
80+
}
81+
}
82+
},
83+
84+
/**
85+
* Using this policy will ensure that numbers will be read as numbers of arbitrary length
86+
* using {@link BigDecimal}.
87+
*/
88+
BIG_DECIMAL {
89+
@Override public BigDecimal readNumber(JsonReader in) throws IOException {
90+
String value = in.nextString();
91+
try {
92+
return new BigDecimal(value);
93+
} catch (NumberFormatException e) {
94+
throw new JsonParseException("Cannot parse " + value, e);
95+
}
96+
}
97+
}
98+
99+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (C) 2021 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.gson;
18+
19+
import java.io.IOException;
20+
21+
import com.google.gson.stream.JsonReader;
22+
23+
/**
24+
* A strategy that is used to control how numbers should be deserialized for {@link Object} and {@link Number}
25+
* when a concrete type of the deserialized number is unknown in advance. By default, Gson uses the following
26+
* deserialization strategies:
27+
*
28+
* <ul>
29+
* <li>{@link Double} values are returned for JSON numbers if the deserialization type is declared as
30+
* {@code Object}, see {@link ToNumberPolicy#DOUBLE};</li>
31+
* <li>Lazily parsed number values are returned if the deserialization type is declared as {@code Number},
32+
* see {@link ToNumberPolicy#LAZILY_PARSED_NUMBER}.</li>
33+
* </ul>
34+
*
35+
* <p>For historical reasons, Gson does not support deserialization of arbitrary-length numbers for
36+
* {@code Object} and {@code Number} by default, potentially causing precision loss. However,
37+
* <a href="https://tools.ietf.org/html/rfc8259#section-6">RFC 8259</a> permits this:
38+
*
39+
* <pre>
40+
* This specification allows implementations to set limits on the range
41+
* and precision of numbers accepted. Since software that implements
42+
* IEEE 754 binary64 (double precision) numbers [IEEE754] is generally
43+
* available and widely used, good interoperability can be achieved by
44+
* implementations that expect no more precision or range than these
45+
* provide, in the sense that implementations will approximate JSON
46+
* numbers within the expected precision. A JSON number such as 1E400
47+
* or 3.141592653589793238462643383279 may indicate potential
48+
* interoperability problems, since it suggests that the software that
49+
* created it expects receiving software to have greater capabilities
50+
* for numeric magnitude and precision than is widely available.
51+
* </pre>
52+
*
53+
* <p>To overcome the precision loss, use for example {@link ToNumberPolicy#LONG_OR_DOUBLE} or
54+
* {@link ToNumberPolicy#BIG_DECIMAL}.</p>
55+
*
56+
* @see ToNumberPolicy
57+
* @see GsonBuilder#setObjectToNumberStrategy(ToNumberStrategy)
58+
* @see GsonBuilder#setNumberToNumberStrategy(ToNumberStrategy)
59+
*/
60+
public interface ToNumberStrategy {
61+
62+
/**
63+
* Reads a number from the given JSON reader. A strategy is supposed to read a single value from the
64+
* reader, and the read value is guaranteed never to be {@code null}.
65+
*
66+
* @param in JSON reader to read a number from
67+
* @return number read from the JSON reader.
68+
* @throws IOException
69+
*/
70+
public Number readNumber(JsonReader in) throws IOException;
71+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (C) 2020 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.gson.internal.bind;
18+
19+
import com.google.gson.Gson;
20+
import com.google.gson.JsonSyntaxException;
21+
import com.google.gson.ToNumberStrategy;
22+
import com.google.gson.ToNumberPolicy;
23+
import com.google.gson.TypeAdapter;
24+
import com.google.gson.TypeAdapterFactory;
25+
import com.google.gson.reflect.TypeToken;
26+
import com.google.gson.stream.JsonReader;
27+
import com.google.gson.stream.JsonToken;
28+
import com.google.gson.stream.JsonWriter;
29+
30+
import java.io.IOException;
31+
32+
/**
33+
* Type adapter for {@link Number}.
34+
*/
35+
public final class NumberTypeAdapter extends TypeAdapter<Number> {
36+
/**
37+
* Gson default factory using {@link ToNumberPolicy#LAZILY_PARSED_NUMBER}.
38+
*/
39+
private static final TypeAdapterFactory LAZILY_PARSED_NUMBER_FACTORY = newFactory(ToNumberPolicy.LAZILY_PARSED_NUMBER);
40+
41+
private final ToNumberStrategy toNumberStrategy;
42+
43+
private NumberTypeAdapter(ToNumberStrategy toNumberStrategy) {
44+
this.toNumberStrategy = toNumberStrategy;
45+
}
46+
47+
private static TypeAdapterFactory newFactory(ToNumberStrategy toNumberStrategy) {
48+
final NumberTypeAdapter adapter = new NumberTypeAdapter(toNumberStrategy);
49+
return new TypeAdapterFactory() {
50+
@SuppressWarnings("unchecked")
51+
@Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
52+
return type.getRawType() == Number.class ? (TypeAdapter<T>) adapter : null;
53+
}
54+
};
55+
}
56+
57+
public static TypeAdapterFactory getFactory(ToNumberStrategy toNumberStrategy) {
58+
if (toNumberStrategy == ToNumberPolicy.LAZILY_PARSED_NUMBER) {
59+
return LAZILY_PARSED_NUMBER_FACTORY;
60+
} else {
61+
return newFactory(toNumberStrategy);
62+
}
63+
}
64+
65+
@Override public Number read(JsonReader in) throws IOException {
66+
JsonToken jsonToken = in.peek();
67+
switch (jsonToken) {
68+
case NULL:
69+
in.nextNull();
70+
return null;
71+
case NUMBER:
72+
case STRING:
73+
return toNumberStrategy.readNumber(in);
74+
default:
75+
throw new JsonSyntaxException("Expecting number, got: " + jsonToken);
76+
}
77+
}
78+
79+
@Override public void write(JsonWriter out, Number value) throws IOException {
80+
out.value(value);
81+
}
82+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy