diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index 30876109a5..927c57662a 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -70,8 +70,9 @@ * The behavior of this reader can be customized with the following methods: * *
This setting overwrites and is overwritten by whether {@link #setStrictness(Strictness)} + * enabled support for multiple top-level values. + * + * @see #isMultiTopLevelValuesAllowed() + * @since $next-version$ + */ + public final void setMultiTopLevelValuesAllowed(boolean enabled) { + this.multiTopLevelValuesEnabled = enabled; + } + + /** + * Returns whether multiple top-level values are allowed. + * + * @see #setMultiTopLevelValuesAllowed(boolean) + * @since $next-version$ + */ + public final boolean isMultiTopLevelValuesAllowed() { + return multiTopLevelValuesEnabled; + } + /** * Sets the nesting limit of this reader. * @@ -661,10 +690,13 @@ int doPeek() throws IOException { int c = nextNonWhitespace(false); if (c == -1) { return peeked = PEEKED_EOF; - } else { - checkLenient(); - pos--; + } else if (!multiTopLevelValuesEnabled) { + throw new MalformedJsonException( + "Multiple top-level values support has not been enabled, use" + + " `JsonReader.setMultiTopLevelValuesAllowed(true)`," + + locationString()); } + pos--; } else if (peekStack == JsonScope.CLOSED) { throw new IllegalStateException("JsonReader is closed"); } diff --git a/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/src/main/java/com/google/gson/stream/JsonWriter.java index 70b0157b24..10f761b70f 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -72,6 +72,7 @@ * output *
This setting overwrites and is overwritten by whether {@link #setStrictness(Strictness)} + * enabled support for multiple top-level values. + * + * @param separator separator between top-level values, or {@code null} to disable. + * @see #getTopLevelSeparator() + * @since $next-version$ + */ + public final void setTopLevelSeparator(String separator) { + this.topLevelSeparator = separator; + } + + /** + * Returns the top-level separator, or {@code null} if disabled. + * + * @see #setTopLevelSeparator(String) + * @since $next-version$ + */ + public final String getTopLevelSeparator() { + return topLevelSeparator; + } + /** * Configures this writer to emit JSON that's safe for direct inclusion in HTML and XML documents. * This escapes the HTML characters {@code <}, {@code >}, {@code &}, {@code =} and {@code '} @@ -807,10 +845,14 @@ private void beforeName() throws IOException { private void beforeValue() throws IOException { switch (peek()) { case NONEMPTY_DOCUMENT: - if (strictness != Strictness.LENIENT) { - throw new IllegalStateException("JSON must have only one top-level value."); + if (topLevelSeparator == null) { + throw new IllegalStateException( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); } - // fall-through + out.append(topLevelSeparator); + break; + case EMPTY_DOCUMENT: // first in document replaceTop(NONEMPTY_DOCUMENT); break; diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index 45cd1d4479..be005424f7 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -411,7 +411,11 @@ public void testNewJsonWriter_Default() throws IOException { // Additional top-level value IllegalStateException e = assertThrows(IllegalStateException.class, () -> jsonWriter.value(1)); - assertThat(e).hasMessageThat().isEqualTo("JSON must have only one top-level value."); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); jsonWriter.close(); assertThat(writer.toString()).isEqualTo("{\"\\u003ctest2\":true}"); diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java index 42d4649683..4d013da82b 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java @@ -195,6 +195,8 @@ public void testOverrides() { "isLenient()", "setStrictness(com.google.gson.Strictness)", "getStrictness()", + "setMultiTopLevelValuesAllowed(boolean)", + "isMultiTopLevelValuesAllowed()", "setNestingLimit(int)", "getNestingLimit()"); MoreAsserts.assertOverridesMethods(JsonReader.class, JsonTreeReader.class, ignoredMethods); diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java index 97dc2e56c0..8d3006e8e0 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java @@ -272,6 +272,8 @@ public void testOverrides() { "isLenient()", "setStrictness(com.google.gson.Strictness)", "getStrictness()", + "setTopLevelSeparator(java.lang.String)", + "getTopLevelSeparator()", "setIndent(java.lang.String)", "setHtmlSafe(boolean)", "isHtmlSafe()", diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java index f8a33be0df..8d055f1f03 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -1460,7 +1460,17 @@ public void testStrictMultipleTopLevelValues() throws IOException { reader.beginArray(); reader.endArray(); var e = assertThrows(MalformedJsonException.class, () -> reader.peek()); - assertStrictError(e, "line 1 column 5 path $"); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonReader.setMultiTopLevelValuesAllowed(true)`, at line 1 column 5 path $"); + + // But trailing whitespace is allowed + JsonReader reader2 = new JsonReader(reader("[] \n \t \r ")); + reader2.beginArray(); + reader2.endArray(); + assertThat(reader2.peek()).isEqualTo(JsonToken.END_DOCUMENT); } @Test @@ -1481,7 +1491,76 @@ public void testStrictMultipleTopLevelValuesWithSkipValue() throws IOException { reader.beginArray(); reader.endArray(); var e = assertThrows(MalformedJsonException.class, () -> reader.skipValue()); - assertStrictError(e, "line 1 column 5 path $"); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonReader.setMultiTopLevelValuesAllowed(true)`, at line 1 column 5 path $"); + } + + @Test + public void testMultipleTopLevelValuesStrictness() { + JsonReader reader = new JsonReader(reader("[]")); + assertThat(reader.isMultiTopLevelValuesAllowed()).isFalse(); + + reader.setStrictness(Strictness.STRICT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isFalse(); + + reader.setStrictness(Strictness.LEGACY_STRICT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isFalse(); + + reader.setStrictness(Strictness.LENIENT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isTrue(); + + reader.setStrictness(Strictness.STRICT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isFalse(); + // Verify that it can be enabled independently of Strictness + reader.setMultiTopLevelValuesAllowed(true); + assertThat(reader.getStrictness()).isEqualTo(Strictness.STRICT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isTrue(); + } + + /** + * Tests multiple top-level values, enabled with {@link + * JsonReader#setMultiTopLevelValuesAllowed(boolean)}. + */ + @Test + public void testMultipleTopLevelValuesEnabled() throws IOException { + JsonReader reader = new JsonReader(reader("[]{}")); + reader.setStrictness(Strictness.STRICT); + reader.setMultiTopLevelValuesAllowed(true); + + reader.beginArray(); + reader.endArray(); + reader.beginObject(); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT); + + reader = new JsonReader(reader("true\n \n1")); + reader.setStrictness(Strictness.STRICT); + reader.setMultiTopLevelValuesAllowed(true); + + assertThat(reader.nextBoolean()).isTrue(); + assertThat(reader.nextInt()).isEqualTo(1); + assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT); + } + + @Test + public void testMultipleTopLevelValuesDisabled() throws IOException { + JsonReader reader = new JsonReader(reader("[]{}")); + // Normally lenient mode allows multiple top-level values + reader.setStrictness(Strictness.LENIENT); + reader.setMultiTopLevelValuesAllowed(false); + + reader.beginArray(); + reader.endArray(); + + var e = assertThrows(MalformedJsonException.class, () -> reader.beginObject()); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonReader.setMultiTopLevelValuesAllowed(true)`, at line 1 column 4 path $"); } @Test diff --git a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java index fd171e880f..2db07386d3 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java @@ -181,7 +181,11 @@ public void testMultipleTopLevelValues() throws IOException { IllegalStateException expected = assertThrows(IllegalStateException.class, jsonWriter::beginArray); - assertThat(expected).hasMessageThat().isEqualTo("JSON must have only one top-level value."); + assertThat(expected) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); } @Test @@ -193,7 +197,11 @@ public void testMultipleTopLevelValuesStrict() throws IOException { IllegalStateException expected = assertThrows(IllegalStateException.class, jsonWriter::beginArray); - assertThat(expected).hasMessageThat().isEqualTo("JSON must have only one top-level value."); + assertThat(expected) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); } @Test @@ -209,6 +217,74 @@ public void testMultipleTopLevelValuesLenient() throws IOException { assertThat(stringWriter.toString()).isEqualTo("[][]"); } + @Test + public void testMultipleTopLevelValuesStrictness() { + JsonWriter writer = new JsonWriter(new StringWriter()); + assertThat(writer.getTopLevelSeparator()).isNull(); + + writer.setStrictness(Strictness.STRICT); + assertThat(writer.getTopLevelSeparator()).isNull(); + + writer.setStrictness(Strictness.LEGACY_STRICT); + assertThat(writer.getTopLevelSeparator()).isNull(); + + writer.setStrictness(Strictness.LENIENT); + assertThat(writer.getTopLevelSeparator()).isEqualTo(""); + + writer.setStrictness(Strictness.STRICT); + assertThat(writer.getTopLevelSeparator()).isNull(); + // Verify that it can be enabled independently of Strictness + writer.setTopLevelSeparator("\n"); + assertThat(writer.getStrictness()).isEqualTo(Strictness.STRICT); + assertThat(writer.getTopLevelSeparator()).isEqualTo("\n"); + } + + /** + * Tests multiple top-level values, enabled with {@link JsonWriter#setTopLevelSeparator(String)}. + */ + @Test + public void testMultipleTopLevelValuesEnabled() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.setStrictness(Strictness.STRICT); + writer.setTopLevelSeparator(""); + + writer.beginArray(); + writer.endArray(); + writer.beginObject(); + writer.endObject(); + writer.close(); + assertThat(stringWriter.toString()).isEqualTo("[]{}"); + + stringWriter = new StringWriter(); + writer = new JsonWriter(stringWriter); + writer.setStrictness(Strictness.STRICT); + writer.setTopLevelSeparator(" \n "); + + writer.value(1); + writer.value(2); + writer.close(); + assertThat(stringWriter.toString()).isEqualTo("1 \n 2"); + } + + @Test + public void testMultipleTopLevelValuesDisabled() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + // Normally lenient mode allows multiple top-level values + writer.setStrictness(Strictness.LENIENT); + writer.setTopLevelSeparator(null); + + writer.value(1); + + var e = assertThrows(IllegalStateException.class, () -> writer.value(2)); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); + } + @Test public void testBadNestingObject() throws IOException { StringWriter stringWriter = new StringWriter();