Skip to content

Commit cbc0af8

Browse files
authored
Improve lenient mode documentation (#2122)
1 parent 3f1d4fb commit cbc0af8

File tree

6 files changed

+185
-36
lines changed

6 files changed

+185
-36
lines changed

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

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,6 @@
1616

1717
package com.google.gson;
1818

19-
import java.io.EOFException;
20-
import java.io.IOException;
21-
import java.io.Reader;
22-
import java.io.StringReader;
23-
import java.io.StringWriter;
24-
import java.io.Writer;
25-
import java.lang.reflect.Type;
26-
import java.math.BigDecimal;
27-
import java.math.BigInteger;
28-
import java.text.DateFormat;
29-
import java.util.ArrayList;
30-
import java.util.Collections;
31-
import java.util.HashMap;
32-
import java.util.List;
33-
import java.util.Map;
34-
import java.util.concurrent.ConcurrentHashMap;
35-
import java.util.concurrent.atomic.AtomicLong;
36-
import java.util.concurrent.atomic.AtomicLongArray;
37-
3819
import com.google.gson.internal.ConstructorConstructor;
3920
import com.google.gson.internal.Excluder;
4021
import com.google.gson.internal.GsonBuildConfig;
@@ -58,6 +39,24 @@
5839
import com.google.gson.stream.JsonToken;
5940
import com.google.gson.stream.JsonWriter;
6041
import com.google.gson.stream.MalformedJsonException;
42+
import java.io.EOFException;
43+
import java.io.IOException;
44+
import java.io.Reader;
45+
import java.io.StringReader;
46+
import java.io.StringWriter;
47+
import java.io.Writer;
48+
import java.lang.reflect.Type;
49+
import java.math.BigDecimal;
50+
import java.math.BigInteger;
51+
import java.text.DateFormat;
52+
import java.util.ArrayList;
53+
import java.util.Collections;
54+
import java.util.HashMap;
55+
import java.util.List;
56+
import java.util.Map;
57+
import java.util.concurrent.ConcurrentHashMap;
58+
import java.util.concurrent.atomic.AtomicLong;
59+
import java.util.concurrent.atomic.AtomicLongArray;
6160

6261
/**
6362
* This is the main class for using Gson. Gson is typically used by first constructing a
@@ -97,6 +96,33 @@
9796
* <p>See the <a href="https://sites.google.com/site/gson/gson-user-guide">Gson User Guide</a>
9897
* for a more complete set of examples.</p>
9998
*
99+
* <h2>Lenient JSON handling</h2>
100+
* For legacy reasons most of the {@code Gson} methods allow JSON data which does not
101+
* comply with the JSON specification, regardless of whether {@link GsonBuilder#setLenient()}
102+
* is used or not. If this behavior is not desired, the following workarounds can be used:
103+
*
104+
* <h3>Serialization</h3>
105+
* <ol>
106+
* <li>Use {@link #getAdapter(Class)} to obtain the adapter for the type to be serialized
107+
* <li>When using an existing {@code JsonWriter}, manually apply the writer settings of this
108+
* {@code Gson} instance listed by {@link #newJsonWriter(Writer)}.<br>
109+
* Otherwise, when not using an existing {@code JsonWriter}, use {@link #newJsonWriter(Writer)}
110+
* to construct one.
111+
* <li>Call {@link TypeAdapter#write(JsonWriter, Object)}
112+
* </ol>
113+
*
114+
* <h3>Deserialization</h3>
115+
* <ol>
116+
* <li>Use {@link #getAdapter(Class)} to obtain the adapter for the type to be deserialized
117+
* <li>When using an existing {@code JsonReader}, manually apply the reader settings of this
118+
* {@code Gson} instance listed by {@link #newJsonReader(Reader)}.<br>
119+
* Otherwise, when not using an existing {@code JsonReader}, use {@link #newJsonReader(Reader)}
120+
* to construct one.
121+
* <li>Call {@link TypeAdapter#read(JsonReader)}
122+
* <li>Call {@link JsonReader#peek()} and verify that the result is {@link JsonToken#END_DOCUMENT}
123+
* to make sure there is no trailing data
124+
* </ol>
125+
*
100126
* @see com.google.gson.reflect.TypeToken
101127
*
102128
* @author Inderjeet Singh
@@ -736,6 +762,15 @@ public void toJson(Object src, Type typeOfSrc, Appendable writer) throws JsonIOE
736762
/**
737763
* Writes the JSON representation of {@code src} of type {@code typeOfSrc} to
738764
* {@code writer}.
765+
*
766+
* <p>The JSON data is written in {@linkplain JsonWriter#setLenient(boolean) lenient mode},
767+
* regardless of the lenient mode setting of the provided writer. The lenient mode setting
768+
* of the writer is restored once this method returns.
769+
*
770+
* <p>The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance
771+
* (configured by the {@link GsonBuilder}) are applied, and the original settings of the
772+
* writer are restored once this method returns.
773+
*
739774
* @throws JsonIOException if there was a problem writing to the writer
740775
*/
741776
@SuppressWarnings("unchecked")
@@ -834,6 +869,15 @@ public JsonReader newJsonReader(Reader reader) {
834869

835870
/**
836871
* Writes the JSON for {@code jsonElement} to {@code writer}.
872+
*
873+
* <p>The JSON data is written in {@linkplain JsonWriter#setLenient(boolean) lenient mode},
874+
* regardless of the lenient mode setting of the provided writer. The lenient mode setting
875+
* of the writer is restored once this method returns.
876+
*
877+
* <p>The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance
878+
* (configured by the {@link GsonBuilder}) are applied, and the original settings of the
879+
* writer are restored once this method returns.
880+
*
837881
* @throws JsonIOException if there was a problem writing to the writer
838882
*/
839883
public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOException {
@@ -868,6 +912,9 @@ public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOExce
868912
* {@link #fromJson(String, Type)}. If you have the Json in a {@link Reader} instead of
869913
* a String, use {@link #fromJson(Reader, Class)} instead.
870914
*
915+
* <p>An exception is thrown if the JSON string has multiple top-level JSON elements,
916+
* or if there is trailing data.
917+
*
871918
* @param <T> the type of the desired object
872919
* @param json the string from which the object is to be deserialized
873920
* @param classOfT the class of T
@@ -887,6 +934,9 @@ public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException
887934
* {@link #fromJson(String, Class)} instead. If you have the Json in a {@link Reader} instead of
888935
* a String, use {@link #fromJson(Reader, Type)} instead.
889936
*
937+
* <p>An exception is thrown if the JSON string has multiple top-level JSON elements,
938+
* or if there is trailing data.
939+
*
890940
* @param <T> the type of the desired object
891941
* @param json the string from which the object is to be deserialized
892942
* @param typeOfT The specific genericized type of src. You can obtain this type by using the
@@ -920,6 +970,9 @@ public <T> T fromJson(String json, Type typeOfT) throws JsonSyntaxException {
920970
* invoke {@link #fromJson(Reader, Type)}. If you have the Json in a String form instead of a
921971
* {@link Reader}, use {@link #fromJson(String, Class)} instead.
922972
*
973+
* <p>An exception is thrown if the JSON data has multiple top-level JSON elements,
974+
* or if there is trailing data.
975+
*
923976
* @param <T> the type of the desired object
924977
* @param json the reader producing the Json from which the object is to be deserialized.
925978
* @param classOfT the class of T
@@ -941,6 +994,9 @@ public <T> T fromJson(Reader json, Class<T> classOfT) throws JsonSyntaxException
941994
* non-generic objects, use {@link #fromJson(Reader, Class)} instead. If you have the Json in a
942995
* String form instead of a {@link Reader}, use {@link #fromJson(String, Type)} instead.
943996
*
997+
* <p>An exception is thrown if the JSON data has multiple top-level JSON elements,
998+
* or if there is trailing data.
999+
*
9441000
* @param <T> the type of the desired object
9451001
* @param json the reader producing Json from which the object is to be deserialized
9461002
* @param typeOfT The specific genericized type of src. You can obtain this type by using the
@@ -965,7 +1021,7 @@ public <T> T fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyn
9651021
private static void assertFullConsumption(Object obj, JsonReader reader) {
9661022
try {
9671023
if (obj != null && reader.peek() != JsonToken.END_DOCUMENT) {
968-
throw new JsonIOException("JSON document was not fully consumed.");
1024+
throw new JsonSyntaxException("JSON document was not fully consumed.");
9691025
}
9701026
} catch (MalformedJsonException e) {
9711027
throw new JsonSyntaxException(e);
@@ -977,7 +1033,14 @@ private static void assertFullConsumption(Object obj, JsonReader reader) {
9771033
/**
9781034
* Reads the next JSON value from {@code reader} and convert it to an object
9791035
* of type {@code typeOfT}. Returns {@code null}, if the {@code reader} is at EOF.
980-
* Since Type is not parameterized by T, this method is type unsafe and should be used carefully
1036+
* Since Type is not parameterized by T, this method is type unsafe and should be used carefully.
1037+
*
1038+
* <p>Unlike the other {@code fromJson} methods, no exception is thrown if the JSON data has
1039+
* multiple top-level JSON elements, or if there is trailing data.
1040+
*
1041+
* <p>The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode},
1042+
* regardless of the lenient mode setting of the provided reader. The lenient mode setting
1043+
* of the reader is restored once this method returns.
9811044
*
9821045
* @throws JsonIOException if there was a problem writing to the Reader
9831046
* @throws JsonSyntaxException if json is not a valid representation for an object of type

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.gson.internal.sql.SqlTypesSupport;
3535
import com.google.gson.reflect.TypeToken;
3636
import com.google.gson.stream.JsonReader;
37+
import com.google.gson.stream.JsonWriter;
3738

3839
import static com.google.gson.Gson.DEFAULT_COMPLEX_MAP_KEYS;
3940
import static com.google.gson.Gson.DEFAULT_DATE_PATTERN;
@@ -425,12 +426,14 @@ public GsonBuilder setPrettyPrinting() {
425426
}
426427

427428
/**
428-
* By default, Gson is strict and only accepts JSON as specified by
429-
* <a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. This option makes the parser
430-
* liberal in what it accepts.
429+
* Configures Gson to allow JSON data which does not strictly comply with the JSON specification.
430+
*
431+
* <p>Note: Due to legacy reasons most methods of Gson are always lenient, regardless of
432+
* whether this builder method is used.
431433
*
432434
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
433435
* @see JsonReader#setLenient(boolean)
436+
* @see JsonWriter#setLenient(boolean)
434437
*/
435438
public GsonBuilder setLenient() {
436439
lenient = true;

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,16 @@
1515
*/
1616
package com.google.gson;
1717

18-
import java.io.IOException;
19-
import java.io.Reader;
20-
import java.io.StringReader;
21-
2218
import com.google.gson.internal.Streams;
2319
import com.google.gson.stream.JsonReader;
2420
import com.google.gson.stream.JsonToken;
2521
import com.google.gson.stream.MalformedJsonException;
22+
import java.io.IOException;
23+
import java.io.Reader;
24+
import java.io.StringReader;
2625

2726
/**
28-
* A parser to parse Json into a parse tree of {@link JsonElement}s
27+
* A parser to parse JSON into a parse tree of {@link JsonElement}s.
2928
*
3029
* @author Inderjeet Singh
3130
* @author Joel Leitch
@@ -37,7 +36,11 @@ public final class JsonParser {
3736
public JsonParser() {}
3837

3938
/**
40-
* Parses the specified JSON string into a parse tree
39+
* Parses the specified JSON string into a parse tree.
40+
* An exception is thrown if the JSON string has multiple top-level JSON elements,
41+
* or if there is trailing data.
42+
*
43+
* <p>The JSON string is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}.
4144
*
4245
* @param json JSON text
4346
* @return a parse tree of {@link JsonElement}s corresponding to the specified JSON
@@ -48,11 +51,16 @@ public static JsonElement parseString(String json) throws JsonSyntaxException {
4851
}
4952

5053
/**
51-
* Parses the specified JSON string into a parse tree
54+
* Parses the complete JSON string provided by the reader into a parse tree.
55+
* An exception is thrown if the JSON string has multiple top-level JSON elements,
56+
* or if there is trailing data.
57+
*
58+
* <p>The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}.
5259
*
5360
* @param reader JSON text
5461
* @return a parse tree of {@link JsonElement}s corresponding to the specified JSON
55-
* @throws JsonParseException if the specified text is not valid JSON
62+
* @throws JsonParseException if there is an IOException or if the specified
63+
* text is not valid JSON
5664
*/
5765
public static JsonElement parseReader(Reader reader) throws JsonIOException, JsonSyntaxException {
5866
try {
@@ -73,6 +81,12 @@ public static JsonElement parseReader(Reader reader) throws JsonIOException, Jso
7381

7482
/**
7583
* Returns the next value from the JSON stream as a parse tree.
84+
* Unlike the other {@code parse} methods, no exception is thrown if the JSON data has
85+
* multiple top-level JSON elements, or if there is trailing data.
86+
*
87+
* <p>The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode},
88+
* regardless of the lenient mode setting of the provided reader. The lenient mode setting
89+
* of the reader is restored once this method returns.
7690
*
7791
* @throws JsonParseException if there is an IOException or if the specified
7892
* text is not valid JSON

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package com.google.gson;
1818

19-
import com.google.gson.internal.bind.JsonTreeWriter;
2019
import com.google.gson.internal.bind.JsonTreeReader;
20+
import com.google.gson.internal.bind.JsonTreeWriter;
2121
import com.google.gson.stream.JsonReader;
2222
import com.google.gson.stream.JsonToken;
2323
import com.google.gson.stream.JsonWriter;
@@ -252,6 +252,9 @@ public final JsonElement toJsonTree(T value) {
252252
* read is strict. Create a {@link JsonReader#setLenient(boolean) lenient}
253253
* {@code JsonReader} and call {@link #read(JsonReader)} for lenient reading.
254254
*
255+
* <p>No exception is thrown if the JSON data has multiple top-level JSON elements,
256+
* or if there is trailing data.
257+
*
255258
* @return the converted Java object. May be null.
256259
* @since 2.2
257260
*/
@@ -266,6 +269,9 @@ public final T fromJson(Reader in) throws IOException {
266269
* strict. Create a {@link JsonReader#setLenient(boolean) lenient} {@code
267270
* JsonReader} and call {@link #read(JsonReader)} for lenient reading.
268271
*
272+
* <p>No exception is thrown if the JSON data has multiple top-level JSON elements,
273+
* or if there is trailing data.
274+
*
269275
* @return the converted Java object. May be null.
270276
* @since 2.2
271277
*/
@@ -276,7 +282,8 @@ public final T fromJson(String json) throws IOException {
276282
/**
277283
* Converts {@code jsonTree} to a Java object.
278284
*
279-
* @param jsonTree the Java object to convert. May be {@link JsonNull}.
285+
* @param jsonTree the JSON element to convert. May be {@link JsonNull}.
286+
* @return the converted Java object. May be null.
280287
* @since 2.2
281288
*/
282289
public final T fromJsonTree(JsonElement jsonTree) {

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,6 @@ public JsonReader(Reader in) {
304304
* prefix</a>, <code>")]}'\n"</code>.
305305
* <li>Streams that include multiple top-level values. With strict parsing,
306306
* each stream must contain exactly one top-level value.
307-
* <li>Top-level values of any type. With strict parsing, the top-level
308-
* value must be an object or an array.
309307
* <li>Numbers may be {@link Double#isNaN() NaNs} or {@link
310308
* Double#isInfinite() infinities}.
311309
* <li>End of line comments starting with {@code //} or {@code #} and
@@ -321,6 +319,18 @@ public JsonReader(Reader in) {
321319
* {@code :}.
322320
* <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
323321
* </ul>
322+
*
323+
* <p>Note: Even in strict mode there are slight derivations from the JSON
324+
* specification:
325+
* <ul>
326+
* <li>JsonReader allows the literals {@code true}, {@code false} and {@code null}
327+
* to have any capitalization, for example {@code fAlSe}
328+
* <li>JsonReader supports the escape sequence {@code \'}, representing a {@code '}
329+
* <li>JsonReader supports the escape sequence <code>\<i>LF</i></code> (with {@code LF}
330+
* being the Unicode character U+000A), resulting in a {@code LF} within the
331+
* read JSON string
332+
* <li>JsonReader allows unescaped control characters (U+0000 through U+001F)
333+
* </ul>
324334
*/
325335
public final void setLenient(boolean lenient) {
326336
this.lenient = lenient;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.google.gson;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNull;
5+
6+
import com.google.gson.stream.JsonReader;
7+
import com.google.gson.stream.JsonWriter;
8+
import java.io.IOException;
9+
import java.io.StringReader;
10+
import org.junit.Test;
11+
12+
public class TypeAdapterTest {
13+
@Test
14+
public void testNullSafe() throws IOException {
15+
TypeAdapter<String> adapter = new TypeAdapter<String>() {
16+
@Override public void write(JsonWriter out, String value) {
17+
throw new AssertionError("unexpected call");
18+
}
19+
20+
@Override public String read(JsonReader in) {
21+
throw new AssertionError("unexpected call");
22+
}
23+
}.nullSafe();
24+
25+
assertEquals("null", adapter.toJson(null));
26+
assertNull(adapter.fromJson("null"));
27+
}
28+
29+
private static final TypeAdapter<String> adapter = new TypeAdapter<String>() {
30+
@Override public void write(JsonWriter out, String value) throws IOException {
31+
out.value(value);
32+
}
33+
34+
@Override public String read(JsonReader in) throws IOException {
35+
return in.nextString();
36+
}
37+
};
38+
39+
// Note: This test just verifies the current behavior; it is a bit questionable
40+
// whether that behavior is actually desired
41+
@Test
42+
public void testFromJson_Reader_TrailingData() throws IOException {
43+
assertEquals("a", adapter.fromJson(new StringReader("\"a\"1")));
44+
}
45+
46+
// Note: This test just verifies the current behavior; it is a bit questionable
47+
// whether that behavior is actually desired
48+
@Test
49+
public void testFromJson_String_TrailingData() throws IOException {
50+
assertEquals("a", adapter.fromJson("\"a\"1"));
51+
}
52+
}

0 commit comments

Comments
 (0)