Skip to content

Commit e2e851c

Browse files
authored
Add LazilyParsedNumber default adapter (#2060)
* Add LazilyParsedNumber default adapter * Validate JsonWriter.value(Number) argument * Fix incorrect JSON number pattern, extend tests
1 parent 710a76c commit e2e851c

File tree

6 files changed

+204
-20
lines changed

6 files changed

+204
-20
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.google.gson.internal.ConstructorConstructor;
3939
import com.google.gson.internal.Excluder;
4040
import com.google.gson.internal.GsonBuildConfig;
41+
import com.google.gson.internal.LazilyParsedNumber;
4142
import com.google.gson.internal.Primitives;
4243
import com.google.gson.internal.Streams;
4344
import com.google.gson.internal.bind.ArrayTypeAdapter;
@@ -267,6 +268,8 @@ public Gson() {
267268
factories.add(TypeAdapters.STRING_BUFFER_FACTORY);
268269
factories.add(TypeAdapters.newFactory(BigDecimal.class, TypeAdapters.BIG_DECIMAL));
269270
factories.add(TypeAdapters.newFactory(BigInteger.class, TypeAdapters.BIG_INTEGER));
271+
// Add adapter for LazilyParsedNumber because user can obtain it from Gson and then try to serialize it again
272+
factories.add(TypeAdapters.newFactory(LazilyParsedNumber.class, TypeAdapters.LAZILY_PARSED_NUMBER));
270273
factories.add(TypeAdapters.URL_FACTORY);
271274
factories.add(TypeAdapters.URI_FACTORY);
272275
factories.add(TypeAdapters.UUID_FACTORY);

gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java

+17
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,23 @@ public void write(JsonWriter out, String value) throws IOException {
436436
}
437437
};
438438

439+
public static final TypeAdapter<LazilyParsedNumber> LAZILY_PARSED_NUMBER = new TypeAdapter<LazilyParsedNumber>() {
440+
// Normally users should not be able to access and deserialize LazilyParsedNumber because
441+
// it is an internal type, but implement this nonetheless in case there are legit corner
442+
// cases where this is possible
443+
@Override public LazilyParsedNumber read(JsonReader in) throws IOException {
444+
if (in.peek() == JsonToken.NULL) {
445+
in.nextNull();
446+
return null;
447+
}
448+
return new LazilyParsedNumber(in.nextString());
449+
}
450+
451+
@Override public void write(JsonWriter out, LazilyParsedNumber value) throws IOException {
452+
out.value(value);
453+
}
454+
};
455+
439456
public static final TypeAdapterFactory STRING_FACTORY = newFactory(String.class, STRING);
440457

441458
public static final TypeAdapter<StringBuilder> STRING_BUILDER = new TypeAdapter<StringBuilder>() {

gson/src/main/java/com/google/gson/stream/JsonWriter.java

+37-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
import java.io.Flushable;
2121
import java.io.IOException;
2222
import java.io.Writer;
23+
import java.math.BigDecimal;
24+
import java.math.BigInteger;
2325
import java.util.Arrays;
26+
import java.util.concurrent.atomic.AtomicInteger;
27+
import java.util.concurrent.atomic.AtomicLong;
28+
import java.util.regex.Pattern;
2429

2530
import static com.google.gson.stream.JsonScope.DANGLING_NAME;
2631
import static com.google.gson.stream.JsonScope.EMPTY_ARRAY;
@@ -130,6 +135,9 @@
130135
*/
131136
public class JsonWriter implements Closeable, Flushable {
132137

138+
// Syntax as defined by https://datatracker.ietf.org/doc/html/rfc8259#section-6
139+
private static final Pattern VALID_JSON_NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][-+]?[0-9]+)?");
140+
133141
/*
134142
* From RFC 7159, "All Unicode characters may be placed within the
135143
* quotation marks except for the characters that must be escaped:
@@ -488,6 +496,8 @@ public JsonWriter value(Boolean value) throws IOException {
488496
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
489497
* {@link Double#isInfinite() infinities}.
490498
* @return this writer.
499+
* @throws IllegalArgumentException if the value is NaN or Infinity and this writer is
500+
* not {@link #setLenient(boolean) lenient}.
491501
*/
492502
public JsonWriter value(double value) throws IOException {
493503
writeDeferredName();
@@ -512,11 +522,26 @@ public JsonWriter value(long value) throws IOException {
512522
}
513523

514524
/**
515-
* Encodes {@code value}.
525+
* Returns whether the {@code toString()} of {@code c} can be trusted to return
526+
* a valid JSON number.
527+
*/
528+
private static boolean isTrustedNumberType(Class<? extends Number> c) {
529+
// Note: Don't consider LazilyParsedNumber trusted because it could contain
530+
// an arbitrary malformed string
531+
return c == Integer.class || c == Long.class || c == Double.class || c == Float.class || c == Byte.class || c == Short.class
532+
|| c == BigDecimal.class || c == BigInteger.class || c == AtomicInteger.class || c == AtomicLong.class;
533+
}
534+
535+
/**
536+
* Encodes {@code value}. The value is written by directly writing the {@link Number#toString()}
537+
* result to JSON. Implementations must make sure that the result represents a valid JSON number.
516538
*
517539
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
518540
* {@link Double#isInfinite() infinities}.
519541
* @return this writer.
542+
* @throws IllegalArgumentException if the value is NaN or Infinity and this writer is
543+
* not {@link #setLenient(boolean) lenient}; or if the {@code toString()} result is not a
544+
* valid JSON number.
520545
*/
521546
public JsonWriter value(Number value) throws IOException {
522547
if (value == null) {
@@ -525,10 +550,18 @@ public JsonWriter value(Number value) throws IOException {
525550

526551
writeDeferredName();
527552
String string = value.toString();
528-
if (!lenient
529-
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
530-
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
553+
if (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN")) {
554+
if (!lenient) {
555+
throw new IllegalArgumentException("Numeric values must be finite, but was " + string);
556+
}
557+
} else {
558+
Class<? extends Number> numberClass = value.getClass();
559+
// Validate that string is valid before writing it directly to JSON output
560+
if (!isTrustedNumberType(numberClass) && !VALID_JSON_NUMBER_PATTERN.matcher(string).matches()) {
561+
throw new IllegalArgumentException("String created by " + numberClass + " is not a valid JSON number: " + string);
562+
}
531563
}
564+
532565
beforeValue();
533566
out.append(string);
534567
return this;

gson/src/test/java/com/google/gson/functional/PrimitiveTest.java

+13
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.gson.JsonPrimitive;
2424
import com.google.gson.JsonSyntaxException;
2525
import com.google.gson.LongSerializationPolicy;
26+
import com.google.gson.internal.LazilyParsedNumber;
2627
import com.google.gson.reflect.TypeToken;
2728
import java.io.Serializable;
2829
import java.io.StringReader;
@@ -393,6 +394,18 @@ public void testBadValueForBigIntegerDeserialization() {
393394
} catch (JsonSyntaxException expected) { }
394395
}
395396

397+
public void testLazilyParsedNumberSerialization() {
398+
LazilyParsedNumber target = new LazilyParsedNumber("1.5");
399+
String actual = gson.toJson(target);
400+
assertEquals("1.5", actual);
401+
}
402+
403+
public void testLazilyParsedNumberDeserialization() {
404+
LazilyParsedNumber expected = new LazilyParsedNumber("1.5");
405+
LazilyParsedNumber actual = gson.fromJson("1.5", LazilyParsedNumber.class);
406+
assertEquals(expected, actual);
407+
}
408+
396409
public void testMoreSpecificSerialization() {
397410
Gson gson = new Gson();
398411
String expected = "This is a string";

gson/src/test/java/com/google/gson/stream/JsonReaderTest.java

+41-11
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public void testInvalidJsonInput() throws IOException {
195195
} catch (IOException expected) {
196196
}
197197
}
198-
198+
199199
@SuppressWarnings("unused")
200200
public void testNulls() {
201201
try {
@@ -311,21 +311,39 @@ public void testDoubles() throws IOException {
311311
+ "1.7976931348623157E308,"
312312
+ "4.9E-324,"
313313
+ "0.0,"
314+
+ "0.00,"
314315
+ "-0.5,"
315316
+ "2.2250738585072014E-308,"
316317
+ "3.141592653589793,"
317-
+ "2.718281828459045]";
318+
+ "2.718281828459045,"
319+
+ "0,"
320+
+ "0.01,"
321+
+ "0e0,"
322+
+ "1e+0,"
323+
+ "1e-0,"
324+
+ "1e0000," // leading 0 is allowed for exponent
325+
+ "1e00001,"
326+
+ "1e+1]";
318327
JsonReader reader = new JsonReader(reader(json));
319328
reader.beginArray();
320329
assertEquals(-0.0, reader.nextDouble());
321330
assertEquals(1.0, reader.nextDouble());
322331
assertEquals(1.7976931348623157E308, reader.nextDouble());
323332
assertEquals(4.9E-324, reader.nextDouble());
324333
assertEquals(0.0, reader.nextDouble());
334+
assertEquals(0.0, reader.nextDouble());
325335
assertEquals(-0.5, reader.nextDouble());
326336
assertEquals(2.2250738585072014E-308, reader.nextDouble());
327337
assertEquals(3.141592653589793, reader.nextDouble());
328338
assertEquals(2.718281828459045, reader.nextDouble());
339+
assertEquals(0.0, reader.nextDouble());
340+
assertEquals(0.01, reader.nextDouble());
341+
assertEquals(0.0, reader.nextDouble());
342+
assertEquals(1.0, reader.nextDouble());
343+
assertEquals(1.0, reader.nextDouble());
344+
assertEquals(1.0, reader.nextDouble());
345+
assertEquals(10.0, reader.nextDouble());
346+
assertEquals(10.0, reader.nextDouble());
329347
reader.endArray();
330348
assertEquals(JsonToken.END_DOCUMENT, reader.peek());
331349
}
@@ -474,6 +492,13 @@ public void testMalformedNumbers() throws IOException {
474492
assertNotANumber("-");
475493
assertNotANumber(".");
476494

495+
// plus sign is not allowed for integer part
496+
assertNotANumber("+1");
497+
498+
// leading 0 is not allowed for integer part
499+
assertNotANumber("00");
500+
assertNotANumber("01");
501+
477502
// exponent lacks digit
478503
assertNotANumber("e");
479504
assertNotANumber("0e");
@@ -508,12 +533,17 @@ public void testMalformedNumbers() throws IOException {
508533
}
509534

510535
private void assertNotANumber(String s) throws IOException {
511-
JsonReader reader = new JsonReader(reader("[" + s + "]"));
536+
JsonReader reader = new JsonReader(reader(s));
512537
reader.setLenient(true);
513-
reader.beginArray();
514538
assertEquals(JsonToken.STRING, reader.peek());
515539
assertEquals(s, reader.nextString());
516-
reader.endArray();
540+
541+
JsonReader strictReader = new JsonReader(reader(s));
542+
try {
543+
strictReader.nextDouble();
544+
fail("Should have failed reading " + s + " as double");
545+
} catch (MalformedJsonException e) {
546+
}
517547
}
518548

519549
public void testPeekingUnquotedStringsPrefixedWithIntegers() throws IOException {
@@ -568,17 +598,17 @@ public void testLongLargerThanMinLongThatWrapsAround() throws IOException {
568598
} catch (NumberFormatException expected) {
569599
}
570600
}
571-
601+
572602
/**
573603
* Issue 1053, negative zero.
574604
* @throws Exception
575605
*/
576606
public void testNegativeZero() throws Exception {
577-
JsonReader reader = new JsonReader(reader("[-0]"));
578-
reader.setLenient(false);
579-
reader.beginArray();
580-
assertEquals(NUMBER, reader.peek());
581-
assertEquals("-0", reader.nextString());
607+
JsonReader reader = new JsonReader(reader("[-0]"));
608+
reader.setLenient(false);
609+
reader.beginArray();
610+
assertEquals(NUMBER, reader.peek());
611+
assertEquals("-0", reader.nextString());
582612
}
583613

584614
/**

0 commit comments

Comments
 (0)