I was quite surprised that while GsonBuilder#setLenient
states
By default, Gson is strict and only accepts JSON as specified by RFC 4627. This option makes the parser liberal in what it accepts.
It appears to be flat-out lie as it is actually always lenient. Moreover, even any call to JsonReader.setLenient(false)
is completely ignored!
After some browsing in numerous related issues and several rejected pull requests "due to legacy compatibility reasons" I've at last found https://github.com/google/gson/issues/1208 with sensible workaround:
JakeWharton commented on 15 Dec 2017
You can call getAdapter(type).fromJson(gson.newJsonReader(input))
instead of just fromJson(input) to get strict parsing. We should
really deprecate all of the fromJson methods and add new versions that
are strict by default.
The reason is bad decisions long ago that we can no longer change ;(
So here is pure Gson solution for strict json object parsing with extensive test case.
import org.junit.Test;
import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import static org.junit.Assert.*;
public class JsonTest {
private static final TypeAdapter<JsonObject> strictGsonObjectAdapter =
new Gson().getAdapter(JsonObject.class);
public static JsonObject parseStrict(String json) {
// https://mcmap.net/q/75703/-how-to-check-if-json-is-valid-in-java-using-gson/47890960#47890960
try {
//return strictGsonObjectAdapter.fromJson(json); // this still allows multiple top level values (
try (JsonReader reader = new JsonReader(new StringReader(json))) {
JsonObject result = strictGsonObjectAdapter.read(reader);
reader.hasNext(); // throws on multiple top level values
return result;
}
} catch (IOException e) {
throw new JsonSyntaxException(e);
}
}
@Test
public void testStrictParsing() {
// https://static.javadoc.io/com.google.code.gson/gson/2.8.5/com/google/gson/stream/JsonReader.html#setLenient-boolean-
// Streams that start with the non-execute prefix, ")]}'\n".
assertThrows(JsonSyntaxException.class, () -> parseStrict("){}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("]{}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("}{}"));
// Streams that include multiple top-level values. With strict parsing, each stream must contain exactly one top-level value.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{}{}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("{}[]null"));
// Top-level values of any type. With strict parsing, the top-level value must be an object or an array.
assertThrows(JsonSyntaxException.class, () -> parseStrict(""));
assertThrows(JsonSyntaxException.class, () -> parseStrict("null"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("Abracadabra"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("13"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("\"literal\""));
assertThrows(JsonSyntaxException.class, () -> parseStrict("[]"));
// Numbers may be NaNs or infinities.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"number\": NaN}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"number\": Infinity}"));
// End of line comments starting with // or # and ending with a newline character.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{//comment\n}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("{#comment\n}"));
// C-style comments starting with /* and ending with */. Such comments may not be nested.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{/*comment*/}"));
// Names that are unquoted or 'single quoted'.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{a: 1}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("{'a': 1}"));
// Strings that are unquoted or 'single quoted'.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\": str}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\": ''}"));
// Array elements separated by ; instead of ,.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\": [1;2]}"));
// Unnecessary array separators. These are interpreted as if null was the omitted value.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\": [1,]}"));
// Names and values separated by = or => instead of :.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\" = 13}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\" => 13}"));
// Name/value pairs separated by ; instead of ,.
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\": 1; \"b\": 2}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\": }"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\": ,}"));
assertThrows(JsonSyntaxException.class, () -> parseStrict("{\"a\": 0,}"));
assertTrue(parseStrict("{} ").entrySet().isEmpty());
assertTrue(parseStrict("{\"a\": null} \n \n").get("a").isJsonNull());
assertEquals(0, parseStrict("{\"a\": 0}").get("a").getAsInt());
assertEquals("", parseStrict("{\"a\": \"\"}").get("a").getAsString());
assertEquals(0, parseStrict("{\"a\": []}").get("a").getAsJsonArray().size());
}
}
Note the this ensures single top level object. It's possible to replace JsonObject.class
with JsonArray.class
or JsonElement.class
to allow top level array or null.
The code above parses JSON to JsonObject
DOM representation.
The code below does strict parsing into custom POJO with conventional fields mapping.
// https://github.com/google/gson/issues/1208
private static final TypeAdapter<Pojo> strictGsonAdapter = new Gson().getAdapter(Pojo.class);
public static Pojo parsePayment(String json) throws IOException {
return strictGsonAdapter.fromJson(json);
}