Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve lenient mode documentation #2122

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
105 changes: 84 additions & 21 deletions gson/src/main/java/com/google/gson/Gson.java
Expand Up @@ -16,25 +16,6 @@

package com.google.gson;

import java.io.EOFException;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicLongArray;

import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.Excluder;
import com.google.gson.internal.GsonBuildConfig;
Expand All @@ -58,6 +39,24 @@
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.google.gson.stream.MalformedJsonException;
import java.io.EOFException;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicLongArray;

/**
* This is the main class for using Gson. Gson is typically used by first constructing a
Expand Down Expand Up @@ -97,6 +96,33 @@
* <p>See the <a href="https://sites.google.com/site/gson/gson-user-guide">Gson User Guide</a>
* for a more complete set of examples.</p>
*
* <h2>Lenient JSON handling</h2>
* For legacy reasons most of the {@code Gson} methods allow JSON data which does not
* comply with the JSON specification, regardless of whether {@link GsonBuilder#setLenient()}
* is used or not. If this behavior is not desired, the following workarounds can be used:
*
* <h3>Serialization</h3>
* <ol>
* <li>Use {@link #getAdapter(Class)} to obtain the adapter for the type to be serialized
* <li>When using an existing {@code JsonWriter}, manually apply the writer settings of this
* {@code Gson} instance listed by {@link #newJsonWriter(Writer)}.<br>
* Otherwise, when not using an existing {@code JsonWriter}, use {@link #newJsonWriter(Writer)}
* to construct one.
* <li>Call {@link TypeAdapter#write(JsonWriter, Object)}
* </ol>
*
* <h3>Deserialization</h3>
* <ol>
* <li>Use {@link #getAdapter(Class)} to obtain the adapter for the type to be deserialized
* <li>When using an existing {@code JsonReader}, manually apply the reader settings of this
* {@code Gson} instance listed by {@link #newJsonReader(Reader)}.<br>
* Otherwise, when not using an existing {@code JsonReader}, use {@link #newJsonReader(Reader)}
* to construct one.
* <li>Call {@link TypeAdapter#read(JsonReader)}
* <li>Call {@link JsonReader#peek()} and verify that the result is {@link JsonToken#END_DOCUMENT}
* to make sure there is no trailing data
* </ol>
*
* @see com.google.gson.reflect.TypeToken
*
* @author Inderjeet Singh
Expand Down Expand Up @@ -736,6 +762,15 @@ public void toJson(Object src, Type typeOfSrc, Appendable writer) throws JsonIOE
/**
* Writes the JSON representation of {@code src} of type {@code typeOfSrc} to
* {@code writer}.
*
* <p>The JSON data is written in {@linkplain JsonWriter#setLenient(boolean) lenient mode},
* regardless of the lenient mode setting of the provided writer. The lenient mode setting
* of the writer is restored once this method returns.
*
* <p>The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance
* (configured by the {@link GsonBuilder}) are applied, and the original settings of the
* writer are restored once this method returns.
*
* @throws JsonIOException if there was a problem writing to the writer
*/
@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -834,6 +869,15 @@ public JsonReader newJsonReader(Reader reader) {

/**
* Writes the JSON for {@code jsonElement} to {@code writer}.
*
* <p>The JSON data is written in {@linkplain JsonWriter#setLenient(boolean) lenient mode},
* regardless of the lenient mode setting of the provided writer. The lenient mode setting
* of the writer is restored once this method returns.
*
* <p>The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance
* (configured by the {@link GsonBuilder}) are applied, and the original settings of the
* writer are restored once this method returns.
*
* @throws JsonIOException if there was a problem writing to the writer
*/
public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOException {
Expand Down Expand Up @@ -868,6 +912,9 @@ public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOExce
* {@link #fromJson(String, Type)}. If you have the Json in a {@link Reader} instead of
* a String, use {@link #fromJson(Reader, Class)} instead.
*
* <p>An exception is thrown if the JSON string has multiple top-level JSON elements,
* or if there is trailing data.
*
* @param <T> the type of the desired object
* @param json the string from which the object is to be deserialized
* @param classOfT the class of T
Expand All @@ -887,6 +934,9 @@ public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException
* {@link #fromJson(String, Class)} instead. If you have the Json in a {@link Reader} instead of
* a String, use {@link #fromJson(Reader, Type)} instead.
*
* <p>An exception is thrown if the JSON string has multiple top-level JSON elements,
* or if there is trailing data.
*
* @param <T> the type of the desired object
* @param json the string from which the object is to be deserialized
* @param typeOfT The specific genericized type of src. You can obtain this type by using the
Expand Down Expand Up @@ -920,6 +970,9 @@ public <T> T fromJson(String json, Type typeOfT) throws JsonSyntaxException {
* invoke {@link #fromJson(Reader, Type)}. If you have the Json in a String form instead of a
* {@link Reader}, use {@link #fromJson(String, Class)} instead.
*
* <p>An exception is thrown if the JSON data has multiple top-level JSON elements,
* or if there is trailing data.
*
* @param <T> the type of the desired object
* @param json the reader producing the Json from which the object is to be deserialized.
* @param classOfT the class of T
Expand All @@ -941,6 +994,9 @@ public <T> T fromJson(Reader json, Class<T> classOfT) throws JsonSyntaxException
* non-generic objects, use {@link #fromJson(Reader, Class)} instead. If you have the Json in a
* String form instead of a {@link Reader}, use {@link #fromJson(String, Type)} instead.
*
* <p>An exception is thrown if the JSON data has multiple top-level JSON elements,
* or if there is trailing data.
*
* @param <T> the type of the desired object
* @param json the reader producing Json from which the object is to be deserialized
* @param typeOfT The specific genericized type of src. You can obtain this type by using the
Expand All @@ -965,7 +1021,7 @@ public <T> T fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyn
private static void assertFullConsumption(Object obj, JsonReader reader) {
try {
if (obj != null && reader.peek() != JsonToken.END_DOCUMENT) {
throw new JsonIOException("JSON document was not fully consumed.");
throw new JsonSyntaxException("JSON document was not fully consumed.");
eamonnmcmanus marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (MalformedJsonException e) {
throw new JsonSyntaxException(e);
Expand All @@ -977,7 +1033,14 @@ private static void assertFullConsumption(Object obj, JsonReader reader) {
/**
* Reads the next JSON value from {@code reader} and convert it to an object
* of type {@code typeOfT}. Returns {@code null}, if the {@code reader} is at EOF.
* Since Type is not parameterized by T, this method is type unsafe and should be used carefully
* Since Type is not parameterized by T, this method is type unsafe and should be used carefully.
*
* <p>Unlike the other {@code fromJson} methods, no exception is thrown if the JSON data has
* multiple top-level JSON elements, or if there is trailing data.
*
* <p>The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode},
* regardless of the lenient mode setting of the provided reader. The lenient mode setting
* of the reader is restored once this method returns.
*
* @throws JsonIOException if there was a problem writing to the Reader
* @throws JsonSyntaxException if json is not a valid representation for an object of type
Expand Down
9 changes: 6 additions & 3 deletions gson/src/main/java/com/google/gson/GsonBuilder.java
Expand Up @@ -34,6 +34,7 @@
import com.google.gson.internal.sql.SqlTypesSupport;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import static com.google.gson.Gson.DEFAULT_COMPLEX_MAP_KEYS;
import static com.google.gson.Gson.DEFAULT_DATE_PATTERN;
Expand Down Expand Up @@ -425,12 +426,14 @@ public GsonBuilder setPrettyPrinting() {
}

/**
* By default, Gson is strict and only accepts JSON as specified by
* <a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. This option makes the parser
* liberal in what it accepts.
* Configures Gson to allow JSON data which does not strictly comply with the JSON specification.
*
* <p>Note: Due to legacy reasons most methods of Gson are always lenient, regardless of
* whether this builder method is used.
*
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @see JsonReader#setLenient(boolean)
* @see JsonWriter#setLenient(boolean)
*/
public GsonBuilder setLenient() {
lenient = true;
Expand Down
30 changes: 22 additions & 8 deletions gson/src/main/java/com/google/gson/JsonParser.java
Expand Up @@ -15,17 +15,16 @@
*/
package com.google.gson;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

import com.google.gson.internal.Streams;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.MalformedJsonException;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

/**
* A parser to parse Json into a parse tree of {@link JsonElement}s
* A parser to parse JSON into a parse tree of {@link JsonElement}s.
*
* @author Inderjeet Singh
* @author Joel Leitch
Expand All @@ -37,7 +36,11 @@ public final class JsonParser {
public JsonParser() {}

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

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

/**
* Returns the next value from the JSON stream as a parse tree.
* Unlike the other {@code parse} methods, no exception is thrown if the JSON data has
* multiple top-level JSON elements, or if there is trailing data.
*
* <p>The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode},
* regardless of the lenient mode setting of the provided reader. The lenient mode setting
* of the reader is restored once this method returns.
*
* @throws JsonParseException if there is an IOException or if the specified
* text is not valid JSON
Expand Down
11 changes: 9 additions & 2 deletions gson/src/main/java/com/google/gson/TypeAdapter.java
Expand Up @@ -16,8 +16,8 @@

package com.google.gson;

import com.google.gson.internal.bind.JsonTreeWriter;
import com.google.gson.internal.bind.JsonTreeReader;
import com.google.gson.internal.bind.JsonTreeWriter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
Expand Down Expand Up @@ -252,6 +252,9 @@ public final JsonElement toJsonTree(T value) {
* read is strict. Create a {@link JsonReader#setLenient(boolean) lenient}
* {@code JsonReader} and call {@link #read(JsonReader)} for lenient reading.
*
* <p>No exception is thrown if the JSON data has multiple top-level JSON elements,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior is probably rather questionable though...

* or if there is trailing data.
*
* @return the converted Java object. May be null.
* @since 2.2
*/
Expand All @@ -266,6 +269,9 @@ public final T fromJson(Reader in) throws IOException {
* strict. Create a {@link JsonReader#setLenient(boolean) lenient} {@code
* JsonReader} and call {@link #read(JsonReader)} for lenient reading.
*
* <p>No exception is thrown if the JSON data has multiple top-level JSON elements,
* or if there is trailing data.
*
* @return the converted Java object. May be null.
* @since 2.2
*/
Expand All @@ -276,7 +282,8 @@ public final T fromJson(String json) throws IOException {
/**
* Converts {@code jsonTree} to a Java object.
*
* @param jsonTree the Java object to convert. May be {@link JsonNull}.
* @param jsonTree the JSON element to convert. May be {@link JsonNull}.
* @return the converted Java object. May be null.
* @since 2.2
*/
public final T fromJsonTree(JsonElement jsonTree) {
Expand Down
14 changes: 12 additions & 2 deletions gson/src/main/java/com/google/gson/stream/JsonReader.java
Expand Up @@ -304,8 +304,6 @@ public JsonReader(Reader in) {
* prefix</a>, <code>")]}'\n"</code>.
* <li>Streams that include multiple top-level values. With strict parsing,
* each stream must contain exactly one top-level value.
* <li>Top-level values of any type. With strict parsing, the top-level
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the changes below in this file are based on #1609 and the comments there.

This specific sentence was outdated because Gson already supports top-level values of other types, even in strict mode.

* value must be an object or an array.
* <li>Numbers may be {@link Double#isNaN() NaNs} or {@link
* Double#isInfinite() infinities}.
* <li>End of line comments starting with {@code //} or {@code #} and
Expand All @@ -321,6 +319,18 @@ public JsonReader(Reader in) {
* {@code :}.
* <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
* </ul>
*
* <p>Note: Even in strict mode there are slight derivations from the JSON
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should clarify that these derivations are not guaranteed behavior and might change in the future?

* specification:
* <ul>
* <li>JsonReader allows the literals {@code true}, {@code false} and {@code null}
* to have any capitalization, for example {@code fAlSe}
* <li>JsonReader supports the escape sequence {@code \'}, representing a {@code '}
* <li>JsonReader supports the escape sequence <code>\<i>LF</i></code> (with {@code LF}
* being the Unicode character U+000A), resulting in a {@code LF} within the
* read JSON string
* <li>JsonReader allows unescaped control characters (U+0000 through U+001F)
* </ul>
*/
public final void setLenient(boolean lenient) {
this.lenient = lenient;
Expand Down
52 changes: 52 additions & 0 deletions gson/src/test/java/com/google/gson/TypeAdapterTest.java
@@ -0,0 +1,52 @@
package com.google.gson;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.io.StringReader;
import org.junit.Test;

public class TypeAdapterTest {
@Test
public void testNullSafe() throws IOException {
TypeAdapter<String> adapter = new TypeAdapter<String>() {
@Override public void write(JsonWriter out, String value) {
throw new AssertionError("unexpected call");
}

@Override public String read(JsonReader in) {
throw new AssertionError("unexpected call");
}
}.nullSafe();

assertEquals("null", adapter.toJson(null));
assertNull(adapter.fromJson("null"));
}

private static final TypeAdapter<String> adapter = new TypeAdapter<String>() {
@Override public void write(JsonWriter out, String value) throws IOException {
out.value(value);
}

@Override public String read(JsonReader in) throws IOException {
return in.nextString();
}
};

// Note: This test just verifies the current behavior; it is a bit questionable
// whether that behavior is actually desired
@Test
public void testFromJson_Reader_TrailingData() throws IOException {
assertEquals("a", adapter.fromJson(new StringReader("\"a\"1")));
}

// Note: This test just verifies the current behavior; it is a bit questionable
// whether that behavior is actually desired
@Test
public void testFromJson_String_TrailingData() throws IOException {
assertEquals("a", adapter.fromJson("\"a\"1"));
}
}