From f6a68f8c922a63a27f2cc5aaa911e8d8a659ce99 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 24 Jul 2022 18:51:05 +0200 Subject: [PATCH 1/5] Fix JsonTreeReader and JsonTreeWriter users not applying original settings Fixes multiple issues where a JsonTreeReader or JsonTreeWriter is used during deserializion / serialization, but the settings from the original JsonReader or JsonWriter (such as lenientness) are not applied. --- .../gson/graph/GraphAdapterBuilder.java | 6 +- .../RuntimeTypeAdapterFactory.java | 15 ++-- .../gson/graph/GraphAdapterBuilderTest.java | 42 +++++++-- .../RuntimeTypeAdapterFactoryTest.java | 69 ++++++++++++++- .../java/com/google/gson/TypeAdapter.java | 48 ++++++++++- .../gson/internal/bind/JsonTreeReader.java | 6 +- .../gson/internal/bind/JsonTreeWriter.java | 8 ++ .../internal/bind/MapTypeAdapterFactory.java | 2 +- .../com/google/gson/stream/JsonReader.java | 2 + .../com/google/gson/stream/JsonWriter.java | 24 +++--- .../java/com/google/gson/TypeAdapterTest.java | 85 +++++++++++++++++++ .../functional/MapAsArrayTypeAdapterTest.java | 34 ++++++++ 12 files changed, 309 insertions(+), 32 deletions(-) diff --git a/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java b/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java index e6a07f141d..e5070a9669 100644 --- a/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java +++ b/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java @@ -194,7 +194,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { // now that we know the typeAdapter for this name, go from JsonElement to 'T' if (element.value == null) { element.typeAdapter = typeAdapter; - element.read(graph); + element.read(graph, in); } return element.value; } finally { @@ -299,12 +299,12 @@ void write(JsonWriter out) throws IOException { typeAdapter.write(out, value); } - void read(Graph graph) throws IOException { + void read(Graph graph, JsonReader originalReader) throws IOException { if (graph.nextCreate != null) { throw new IllegalStateException("Unexpected recursive call to read() for " + id); } graph.nextCreate = this; - value = typeAdapter.fromJsonTree(element); + value = typeAdapter.fromJsonTreeWithSettingsFrom(element, originalReader); if (value == null) { throw new IllegalStateException("non-null value deserialized to null: " + element); } diff --git a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index a8c6368c28..546e1e018a 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -151,12 +151,15 @@ private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boole /** * Creates a new runtime type adapter using for {@code baseType} using {@code * typeFieldName} as the type field name. Type field names are case sensitive. - * {@code maintainType} flag decide if the type will be stored in pojo or not. + * {@code maintainType} flag decides if during deserialization the type field + * is kept ({@code true}) or removed ({@code false}), and whether during + * serialization it is added by this factory ({@code false}) or assumed to be + * already present for the class ({@code true}). */ public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); } - + /** * Creates a new runtime type adapter using for {@code baseType} using {@code * typeFieldName} as the type field name. Type field names are case sensitive. @@ -227,7 +230,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { } else { labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); } - + if (labelJsonElement == null) { throw new JsonParseException("cannot deserialize " + baseType + " because it does not define a field named " + typeFieldName); @@ -239,7 +242,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + label + "; did you forget to register a subtype?"); } - return delegate.fromJsonTree(jsonElement); + return delegate.fromJsonTreeWithSettingsFrom(jsonElement, in); } @Override public void write(JsonWriter out, R value) throws IOException { @@ -251,7 +254,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { throw new JsonParseException("cannot serialize " + srcType.getName() + "; did you forget to register a subtype?"); } - JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + JsonObject jsonObject = delegate.toJsonTreeWithSettingsFrom(value, out).getAsJsonObject(); if (maintainType) { jsonElementAdapter.write(out, jsonObject); @@ -265,7 +268,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { + " because it already defines a field named " + typeFieldName); } clone.add(typeFieldName, new JsonPrimitive(label)); - + for (Map.Entry e : jsonObject.entrySet()) { clone.add(e.getKey(), e.getValue()); } diff --git a/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java b/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java index 3b2425ec9e..795611d66a 100644 --- a/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java +++ b/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java @@ -18,18 +18,21 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import java.io.IOException; +import java.io.StringReader; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.List; - import org.junit.Test; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; - public final class GraphAdapterBuilderTest { @Test public void testSerialization() { @@ -88,6 +91,35 @@ public void testDeserializationDirectSelfReference() { assertSame(suicide, suicide.beats); } + static class DoubleContainer { + double d; + } + + @Test + public void testDeserializationLenientness() throws IOException { + String json = "{\"0x1\":{\"d\":\"NaN\"}}"; + + GsonBuilder gsonBuilder = new GsonBuilder(); + new GraphAdapterBuilder() + .addType(DoubleContainer.class) + .registerOn(gsonBuilder); + // Use TypeAdapter to avoid default lenientness of Gson + TypeAdapter adapter = gsonBuilder.create().getAdapter(DoubleContainer.class); + + try { + adapter.read(new JsonReader(new StringReader(json))); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage()); + } + + JsonReader lenientReader = new JsonReader(new StringReader(json)); + lenientReader.setLenient(true); + + DoubleContainer deserialized = adapter.read(lenientReader); + assertEquals((Double) Double.NaN, (Double) deserialized.d); + } + @Test public void testSerializeListOfLists() { Type listOfListsType = new TypeToken>>() {}.getType(); diff --git a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java index e58ee0f9c3..7869f27ba0 100644 --- a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java +++ b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java @@ -19,7 +19,14 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; import junit.framework.TestCase; public final class RuntimeTypeAdapterFactoryTest extends TestCase { @@ -172,10 +179,10 @@ public void testSerializeCollidingTypeFieldName() { public void testSerializeWrappedNullValue() { TypeAdapterFactory billingAdapter = RuntimeTypeAdapterFactory.of(BillingInstrument.class) .registerSubtype(CreditCard.class) - .registerSubtype(BankTransfer.class); + .registerSubtype(BankTransfer.class); Gson gson = new GsonBuilder() .registerTypeAdapterFactory(billingAdapter) - .create(); + .create(); String serialized = gson.toJson(new BillingInstrumentWrapper(null), BillingInstrumentWrapper.class); BillingInstrumentWrapper deserialized = gson.fromJson(serialized, BillingInstrumentWrapper.class); assertNull(deserialized.instrument); @@ -210,4 +217,62 @@ static class BankTransfer extends BillingInstrument { this.bankAccount = bankAccount; } } + + public void testDeserializeReaderSettings() throws IOException { + // Directly use TypeAdapter to avoid default lenientness of Gson + TypeAdapter adapter = RuntimeTypeAdapterFactory.of(DummyBaseClass.class, "type", true) + .registerSubtype(DoubleContainer.class, "d") + .create(new Gson(), TypeToken.get(DummyBaseClass.class)); + + String json = "{\"type\":\"d\",\"d\":\"NaN\"}"; + try { + adapter.read(new JsonReader(new StringReader(json))); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage()); + } + + JsonReader lenientReader = new JsonReader(new StringReader(json)); + lenientReader.setLenient(true); + DoubleContainer deserialized = (DoubleContainer) adapter.read(lenientReader); + assertEquals((Double) Double.NaN, deserialized.d); + } + + public void testSerializeWriterSettings() throws IOException { + Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + // Directly use TypeAdapter to avoid default lenientness of Gson + TypeAdapter adapter = RuntimeTypeAdapterFactory.of(DummyBaseClass.class, "type") + .registerSubtype(DoubleContainer.class, "d") + .create(gson, TypeToken.get(DummyBaseClass.class)); + + String json = adapter.toJson(new DoubleContainer(1.0)); + assertEquals("{\"type\":\"d\",\"d\":1.0,\"d2\":null}", json); + + try { + adapter.toJson(new DoubleContainer(Double.NaN)); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage()); + } + + StringWriter writer = new StringWriter(); + JsonWriter customWriter = new JsonWriter(writer); + customWriter.setLenient(true); + customWriter.setSerializeNulls(false); + + adapter.write(customWriter, new DoubleContainer(Double.NaN)); + assertEquals("{\"type\":\"d\",\"d\":NaN}", writer.toString()); + } + + static class DummyBaseClass { + } + + static class DoubleContainer extends DummyBaseClass { + Double d; + Double d2; + + DoubleContainer(Double d) { + this.d = d; + } + } } diff --git a/gson/src/main/java/com/google/gson/TypeAdapter.java b/gson/src/main/java/com/google/gson/TypeAdapter.java index ba798537be..35d5755fad 100644 --- a/gson/src/main/java/com/google/gson/TypeAdapter.java +++ b/gson/src/main/java/com/google/gson/TypeAdapter.java @@ -222,7 +222,9 @@ public final String toJson(T value) { } /** - * Converts {@code value} to a JSON tree. + * Converts {@code value} to a JSON tree. The internally used writer is + * {@linkplain JsonWriter#setLenient(boolean) strict} and + * {@linkplain JsonWriter#setSerializeNulls(boolean) serializes null}. * * @param value the Java object to convert. May be null. * @return the converted JSON tree. May be {@link JsonNull}. @@ -238,6 +240,26 @@ public final JsonElement toJsonTree(T value) { } } + /** + * Converts {@code value} to a JSON tree, with the settings such as the + * {@linkplain JsonWriter#setLenient(boolean) lenient mode} applied from + * the given writer. + * + * @param value the Java object to convert. May be null. + * @param otherWriter whose settings should be used for serialization + * @return the converted JSON tree. May be {@link JsonNull}. + */ + public final JsonElement toJsonTreeWithSettingsFrom(T value, JsonWriter otherWriter) { + try { + JsonTreeWriter jsonWriter = new JsonTreeWriter(); + jsonWriter.applySettingsFrom(otherWriter); + write(jsonWriter, value); + return jsonWriter.get(); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + /** * Reads one JSON value (an array, object, string, number, boolean or null) * and converts it to a Java object. Returns the converted object. @@ -280,7 +302,8 @@ public final T fromJson(String json) throws IOException { } /** - * Converts {@code jsonTree} to a Java object. + * Converts {@code jsonTree} to a Java object. The internally used reader is + * strict and does not allow non-finite floating point values. * * @param jsonTree the JSON element to convert. May be {@link JsonNull}. * @return the converted Java object. May be null. @@ -288,7 +311,26 @@ public final T fromJson(String json) throws IOException { */ public final T fromJsonTree(JsonElement jsonTree) { try { - JsonReader jsonReader = new JsonTreeReader(jsonTree); + JsonTreeReader jsonReader = new JsonTreeReader(jsonTree); + return read(jsonReader); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + + /** + * Converts {@code jsonTree} to a Java object, with the settings such as the + * {@linkplain JsonReader#setLenient(boolean) lenient mode} applied from + * the given reader. + * + * @param jsonTree the JSON element to convert. May be {@link JsonNull}. + * @param otherReader whose settings should be used for deserialization + * @return the converted Java object. May be null. + */ + public final T fromJsonTreeWithSettingsFrom(JsonElement jsonTree, JsonReader otherReader) { + try { + JsonTreeReader jsonReader = new JsonTreeReader(jsonTree); + jsonReader.applySettingsFrom(otherReader); return read(jsonReader); } catch (IOException e) { throw new JsonIOException(e); diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java index a753402ed1..da31bb474f 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java @@ -25,9 +25,9 @@ import com.google.gson.stream.JsonToken; import java.io.IOException; import java.io.Reader; +import java.util.Arrays; import java.util.Iterator; import java.util.Map; -import java.util.Arrays; /** * This reader walks the elements of a JsonElement as if it was coming from a @@ -68,6 +68,10 @@ public JsonTreeReader(JsonElement element) { push(element); } + public void applySettingsFrom(JsonReader reader) { + setLenient(reader.isLenient()); + } + @Override public void beginArray() throws IOException { expect(JsonToken.BEGIN_ARRAY); JsonArray array = (JsonArray) peekStack(); diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java index e28fbfeb34..0938095472 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java @@ -58,6 +58,14 @@ public JsonTreeWriter() { super(UNWRITABLE_WRITER); } + /** + * Applies all settings relevant for {@code JsonTreeWriter} from the given writer. + */ + public void applySettingsFrom(JsonWriter writer) { + setLenient(writer.isLenient()); + setSerializeNulls(writer.getSerializeNulls()); + } + /** * Returns the top level object produced by this writer. */ diff --git a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java index f7c5a55464..af134c36f4 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java @@ -215,7 +215,7 @@ public Adapter(Gson context, Type keyType, TypeAdapter keyTypeAdapter, List values = new ArrayList<>(map.size()); for (Map.Entry entry : map.entrySet()) { - JsonElement keyElement = keyTypeAdapter.toJsonTree(entry.getKey()); + JsonElement keyElement = keyTypeAdapter.toJsonTreeWithSettingsFrom(entry.getKey(), out); keys.add(keyElement); values.add(entry.getValue()); hasComplexKeys |= keyElement.isJsonArray() || keyElement.isJsonObject(); 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 6cb820bef7..194ed63ad9 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -228,6 +228,8 @@ public class JsonReader implements Closeable { /** True to accept non-spec compliant JSON */ private boolean lenient = false; + //Important: When adding more settings, adjust JsonTreeReader.applySettingsFrom, if necessary + static final int BUFFER_SIZE = 1024; /** * Use a manual buffer to easily read and unread upcoming characters, and 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 c281009cbc..f17f4aa252 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -16,6 +16,14 @@ package com.google.gson.stream; +import static com.google.gson.stream.JsonScope.DANGLING_NAME; +import static com.google.gson.stream.JsonScope.EMPTY_ARRAY; +import static com.google.gson.stream.JsonScope.EMPTY_DOCUMENT; +import static com.google.gson.stream.JsonScope.EMPTY_OBJECT; +import static com.google.gson.stream.JsonScope.NONEMPTY_ARRAY; +import static com.google.gson.stream.JsonScope.NONEMPTY_DOCUMENT; +import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT; + import java.io.Closeable; import java.io.Flushable; import java.io.IOException; @@ -27,14 +35,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; -import static com.google.gson.stream.JsonScope.DANGLING_NAME; -import static com.google.gson.stream.JsonScope.EMPTY_ARRAY; -import static com.google.gson.stream.JsonScope.EMPTY_DOCUMENT; -import static com.google.gson.stream.JsonScope.EMPTY_OBJECT; -import static com.google.gson.stream.JsonScope.NONEMPTY_ARRAY; -import static com.google.gson.stream.JsonScope.NONEMPTY_DOCUMENT; -import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT; - /** * Writes a JSON (RFC 7159) * encoded value to a stream, one token at a time. The stream includes both @@ -198,6 +198,8 @@ public class JsonWriter implements Closeable, Flushable { private boolean serializeNulls = true; + // Important: When adding more settings, adjust JsonTreeWriter.applySettingsFrom, if necessary + /** * Creates a new instance that writes a JSON-encoded stream to {@code out}. * For best performance, ensure {@link Writer} is buffered; wrapping in @@ -213,8 +215,8 @@ public JsonWriter(Writer out) { /** * Sets the indentation string to be repeated for each level of indentation * in the encoded document. If {@code indent.isEmpty()} the encoded document - * will be compact. Otherwise the encoded document will be more - * human-readable. + * will be compact, this is the default. Otherwise the encoded document will + * be more human-readable. * * @param indent a string containing only whitespace. */ @@ -256,7 +258,7 @@ public boolean isLenient() { * and XML documents. This escapes the HTML characters {@code <}, {@code >}, * {@code &} and {@code =} before writing them to the stream. Without this * setting, your XML/HTML encoder should replace these characters with the - * corresponding escape sequences. + * corresponding escape sequences. By default this writer is not HTML-safe. */ public final void setHtmlSafe(boolean htmlSafe) { this.htmlSafe = htmlSafe; diff --git a/gson/src/test/java/com/google/gson/TypeAdapterTest.java b/gson/src/test/java/com/google/gson/TypeAdapterTest.java index ab44637393..d6fa2ca09e 100644 --- a/gson/src/test/java/com/google/gson/TypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/TypeAdapterTest.java @@ -2,11 +2,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.io.StringReader; +import java.io.StringWriter; import org.junit.Test; public class TypeAdapterTest { @@ -49,4 +51,87 @@ public void testFromJson_Reader_TrailingData() throws IOException { public void testFromJson_String_TrailingData() throws IOException { assertEquals("a", adapter.fromJson("\"a\"1")); } + + private static final TypeAdapter customDoubleAdapter = new TypeAdapter() { + @Override public void write(JsonWriter out, Double value) throws IOException { + out.beginObject(); + out.name("d"); + out.value(value); + out.endObject(); + } + + @Override public Double read(JsonReader in) throws IOException { + // Note: Does not match `write` method above because tests don't require that + return in.nextDouble(); + } + }; + + @Test + public void testToJsonTree() { + JsonObject expectedJson = new JsonObject(); + expectedJson.add("d", JsonNull.INSTANCE); + + assertEquals(expectedJson, customDoubleAdapter.toJsonTree(null)); + + try { + customDoubleAdapter.toJsonTree(Double.NaN); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage()); + } + } + + @Test + public void testToJsonTreeWithSettingsFrom() { + JsonWriter customWriter = new JsonWriter(new StringWriter()); + + JsonObject expectedJson = new JsonObject(); + expectedJson.add("d", JsonNull.INSTANCE); + + assertEquals(expectedJson, customDoubleAdapter.toJsonTreeWithSettingsFrom(null, customWriter)); + + try { + customDoubleAdapter.toJsonTreeWithSettingsFrom(Double.NaN, customWriter); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage()); + } + + customWriter.setLenient(true); + customWriter.setSerializeNulls(false); + + // Should be empty JSON object + assertEquals(new JsonObject(), customDoubleAdapter.toJsonTreeWithSettingsFrom(null, customWriter)); + + expectedJson = new JsonObject(); + expectedJson.addProperty("d", Double.NaN); + assertEquals(expectedJson, customDoubleAdapter.toJsonTreeWithSettingsFrom(Double.NaN, customWriter)); + } + + @Test + public void testFromJsonTree() { + try { + customDoubleAdapter.fromJsonTree(new JsonPrimitive(Double.NaN)); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage()); + } + } + + @Test + public void testFromJsonTreeWithSettingsFrom() { + JsonReader customReader = new JsonReader(new StringReader("")); + + try { + customDoubleAdapter.fromJsonTreeWithSettingsFrom(new JsonPrimitive(Double.NaN), customReader); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage()); + } + + customReader.setLenient(true); + + Double deserialized = customDoubleAdapter.fromJsonTreeWithSettingsFrom(new JsonPrimitive(Double.NaN), customReader); + assertEquals((Double) Double.NaN, deserialized); + } } diff --git a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java index 114c94ec4f..209bf7e9e3 100644 --- a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java @@ -20,6 +20,8 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonWriter; +import java.io.StringWriter; import java.lang.reflect.Type; import java.util.HashMap; import java.util.LinkedHashMap; @@ -138,4 +140,36 @@ static class Point { static class PointWithProperty { Map map = new HashMap<>(); } + + /** + * Complex map key serialization should use same {@link JsonWriter} settings are + * originally provided writer. + */ + public void testCustomJsonWriter() { + Gson gson = new GsonBuilder() + .enableComplexMapKeySerialization() + .serializeSpecialFloatingPointValues() + .create(); + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setHtmlSafe(true); + jsonWriter.setLenient(true); + jsonWriter.setSerializeNulls(false); + + Map map = new HashMap<>(); + map.put(new DoubleContainer(null), 1); + map.put(new DoubleContainer(Double.NaN), 2); + + Type type = new TypeToken>() {}.getType(); + gson.toJson(map, type, jsonWriter); + assertEquals("[[{},1],[{\"d\":NaN},2]]", writer.toString()); + } + + static class DoubleContainer { + Double d = Double.NaN; + + DoubleContainer(Double d) { + this.d = d; + } + } } From ad34633d8002275a550e4bf1d21a8808fd46858d Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 24 Jul 2022 20:28:09 +0200 Subject: [PATCH 2/5] Properly handle temporary JsonWriter settings changes --- .../RuntimeTypeAdapterFactory.java | 29 +++++- .../RuntimeTypeAdapterFactoryTest.java | 88 ++++++++++++++++--- .../java/com/google/gson/TypeAdapter.java | 22 +++++ .../internal/bind/MapTypeAdapterFactory.java | 24 ++++- .../functional/MapAsArrayTypeAdapterTest.java | 81 +++++++++++++++-- 5 files changed, 226 insertions(+), 18 deletions(-) diff --git a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index 546e1e018a..ad64a9c517 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -257,7 +257,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { JsonObject jsonObject = delegate.toJsonTreeWithSettingsFrom(value, out).getAsJsonObject(); if (maintainType) { - jsonElementAdapter.write(out, jsonObject); + writeObjectPermissively(out, jsonObject); return; } @@ -272,7 +272,32 @@ public TypeAdapter create(Gson gson, TypeToken type) { for (Map.Entry e : jsonObject.entrySet()) { clone.add(e.getKey(), e.getValue()); } - jsonElementAdapter.write(out, clone); + writeObjectPermissively(out, clone); + } + + private void writeObjectPermissively(JsonWriter out, JsonObject object) throws IOException { + /* + * When object was written to JsonObject its adapter might have temporarily overwritten + * JsonWriter settings. Cannot know which settings it used, therefore when writing + * JsonObject here, make it as permissive as possible. + * + * This has no effect if adapter did not change settings. Then JsonObject was written + * with same settings as `out` and the following temporary setting changes won't make + * a difference. + * + * Unfortunately this workaround won't work for HTML-safe and indentation settings, + * though at least they do not affect the JSON data, only the formatting. + */ + boolean oldLenient = out.isLenient(); + boolean oldSerializeNulls = out.getSerializeNulls(); + try { + out.setLenient(true); + out.setSerializeNulls(true); + jsonElementAdapter.write(out, object); + } finally { + out.setLenient(oldLenient); + out.setSerializeNulls(oldSerializeNulls); + } } }.nullSafe(); } diff --git a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java index 7869f27ba0..9af7d68822 100644 --- a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java +++ b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java @@ -21,7 +21,6 @@ import com.google.gson.JsonParseException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; @@ -219,10 +218,12 @@ static class BankTransfer extends BillingInstrument { } public void testDeserializeReaderSettings() throws IOException { - // Directly use TypeAdapter to avoid default lenientness of Gson - TypeAdapter adapter = RuntimeTypeAdapterFactory.of(DummyBaseClass.class, "type", true) - .registerSubtype(DoubleContainer.class, "d") - .create(new Gson(), TypeToken.get(DummyBaseClass.class)); + Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory + .of(DummyBaseClass.class, "type", true).registerSubtype(DoubleContainer.class, "d")) + .create(); + // Use TypeAdapter to avoid default lenientness of Gson + TypeAdapter adapter = gson.getAdapter(DummyBaseClass.class); String json = "{\"type\":\"d\",\"d\":\"NaN\"}"; try { @@ -239,11 +240,13 @@ public void testDeserializeReaderSettings() throws IOException { } public void testSerializeWriterSettings() throws IOException { - Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); - // Directly use TypeAdapter to avoid default lenientness of Gson - TypeAdapter adapter = RuntimeTypeAdapterFactory.of(DummyBaseClass.class, "type") - .registerSubtype(DoubleContainer.class, "d") - .create(gson, TypeToken.get(DummyBaseClass.class)); + Gson gson = new GsonBuilder() + .serializeSpecialFloatingPointValues() + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory + .of(DummyBaseClass.class, "type").registerSubtype(DoubleContainer.class, "d")) + .create(); + // Use TypeAdapter to avoid default lenientness of Gson + TypeAdapter adapter = gson.getAdapter(DummyBaseClass.class); String json = adapter.toJson(new DoubleContainer(1.0)); assertEquals("{\"type\":\"d\",\"d\":1.0,\"d2\":null}", json); @@ -264,6 +267,71 @@ public void testSerializeWriterSettings() throws IOException { assertEquals("{\"type\":\"d\",\"d\":NaN}", writer.toString()); } + /** + * Tests serialization behavior when custom adapter temporarily modifies {@link JsonWriter}. + */ + public void testSerializeAdapterOverwriting() throws IOException { + Gson gson = new GsonBuilder() + .registerTypeAdapter(DoubleContainer.class, new TypeAdapter() { + @Override public void write(JsonWriter out, DoubleContainer value) throws IOException { + boolean oldLenient = out.isLenient(); + boolean oldSerializeNulls = out.getSerializeNulls(); + try { + out.setLenient(true); + out.setSerializeNulls(true); + + out.beginObject(); + out.name("c1"); + out.value(Double.NaN); + out.name("c2"); + out.nullValue(); + out.endObject(); + } finally { + out.setLenient(oldLenient); + out.setSerializeNulls(oldSerializeNulls); + } + } + + @Override public DoubleContainer read(JsonReader in) throws IOException { + throw new AssertionError("not used by this test"); + } + }) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory + .of(DummyBaseClass.class, "type").registerSubtype(DoubleContainer.class, "d")) + .create(); + // Use TypeAdapter to avoid default lenientness of Gson + TypeAdapter adapter = gson.getAdapter(DoubleContainer.class); + + String expectedJson = "{\"type\":\"d\",\"c1\":NaN,\"c2\":null}"; + + // First create a permissive writer + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setSerializeNulls(true); + jsonWriter.setLenient(true); + + adapter.write(jsonWriter, new DoubleContainer(0.0)); + assertEquals(expectedJson, writer.toString()); + + // Should still have original settings values + assertEquals(true, jsonWriter.getSerializeNulls()); + assertEquals(true, jsonWriter.isLenient()); + + // Then try non-permissive writer; should have same result because custom + // adapter temporarily changed writer settings + writer = new StringWriter(); + jsonWriter = new JsonWriter(writer); + jsonWriter.setSerializeNulls(false); + jsonWriter.setLenient(false); + + adapter.write(jsonWriter, new DoubleContainer(0.0)); + assertEquals(expectedJson, writer.toString()); + + // Should still have original settings values + assertEquals(false, jsonWriter.getSerializeNulls()); + assertEquals(false, jsonWriter.isLenient()); + } + static class DummyBaseClass { } diff --git a/gson/src/main/java/com/google/gson/TypeAdapter.java b/gson/src/main/java/com/google/gson/TypeAdapter.java index 35d5755fad..3bacab3883 100644 --- a/gson/src/main/java/com/google/gson/TypeAdapter.java +++ b/gson/src/main/java/com/google/gson/TypeAdapter.java @@ -245,6 +245,28 @@ public final JsonElement toJsonTree(T value) { * {@linkplain JsonWriter#setLenient(boolean) lenient mode} applied from * the given writer. * + *

Note: In case the result of this method is afterwards written + * to {@code otherWriter}, possibly after some modifications to the + * {@code JsonElement}, it might be necessary to temporarily reconfigure + * {@code otherWriter}: + *

{@code
+   *boolean oldLenient = otherWriter.isLenient();
+   *boolean oldSerializeNulls = otherWriter.getSerializeNulls();
+   *try {
+   *  otherWriter.setLenient(true);
+   *  otherWriter.setSerializeNulls(true);
+   *
+   *  ... // write JsonElement result to otherWriter
+   *
+   *} finally {
+   *  otherWriter.setLenient(oldLenient);
+   *  otherWriter.setSerializeNulls(oldSerializeNulls);
+   *}
+   * }
+ * This is necessary because this type adapter might have temporarily + * changed the settings of the internally used writer during serialization, + * for example to make it lenient. + * * @param value the Java object to convert. May be null. * @param otherWriter whose settings should be used for serialization * @return the converted JSON tree. May be {@link JsonNull}. diff --git a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java index af134c36f4..d5eb873941 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java @@ -225,7 +225,29 @@ public Adapter(Gson context, Type keyType, TypeAdapter keyTypeAdapter, out.beginArray(); for (int i = 0, size = keys.size(); i < size; i++) { out.beginArray(); // entry array - Streams.write(keys.get(i), out); + + /* + * When key was written to JsonElement its adapter might have temporarily overwritten + * JsonWriter settings. Cannot know which settings it used, therefore when writing + * JsonElement here, make it as permissive as possible. + * + * This has no effect if adapter did not change settings. Then JsonElement was written + * with same settings as `out` and the following temporary setting changes won't make + * a difference. + * + * Unfortunately this workaround won't work for HTML-safe and indentation settings, + * though at least they do not affect the JSON data, only the formatting. + */ + boolean oldLenient = out.isLenient(); + boolean oldSerializeNulls = out.getSerializeNulls(); + try { + out.setLenient(true); + out.setSerializeNulls(true); + Streams.write(keys.get(i), out); + } finally { + out.setLenient(oldLenient); + out.setSerializeNulls(oldSerializeNulls); + } valueTypeAdapter.write(out, values.get(i)); out.endArray(); } diff --git a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java index 209bf7e9e3..5c79b37b53 100644 --- a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java @@ -19,8 +19,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.Type; import java.util.HashMap; @@ -142,17 +145,16 @@ static class PointWithProperty { } /** - * Complex map key serialization should use same {@link JsonWriter} settings are + * Complex map key serialization should use same {@link JsonWriter} settings as * originally provided writer. */ - public void testCustomJsonWriter() { + public void testCustomJsonWriter() throws IOException { Gson gson = new GsonBuilder() .enableComplexMapKeySerialization() .serializeSpecialFloatingPointValues() .create(); StringWriter writer = new StringWriter(); JsonWriter jsonWriter = new JsonWriter(writer); - jsonWriter.setHtmlSafe(true); jsonWriter.setLenient(true); jsonWriter.setSerializeNulls(false); @@ -160,11 +162,80 @@ public void testCustomJsonWriter() { map.put(new DoubleContainer(null), 1); map.put(new DoubleContainer(Double.NaN), 2); - Type type = new TypeToken>() {}.getType(); - gson.toJson(map, type, jsonWriter); + // Use TypeAdapter to avoid default lenientness of Gson + TypeAdapter> adapter = gson.getAdapter(new TypeToken>() {}); + adapter.write(jsonWriter, map); assertEquals("[[{},1],[{\"d\":NaN},2]]", writer.toString()); } + /** + * Tests serialization behavior when custom adapter temporarily modifies {@link JsonWriter}. + */ + public void testSerializeAdapterOverwriting() throws IOException { + Gson gson = new GsonBuilder() + .enableComplexMapKeySerialization() + .registerTypeAdapter(DoubleContainer.class, new TypeAdapter() { + @Override public void write(JsonWriter out, DoubleContainer value) throws IOException { + boolean oldLenient = out.isLenient(); + boolean oldSerializeNulls = out.getSerializeNulls(); + try { + out.setLenient(true); + out.setSerializeNulls(true); + + out.beginObject(); + out.name("c1"); + out.value(Double.NaN); + out.name("c2"); + out.nullValue(); + out.endObject(); + } finally { + out.setLenient(oldLenient); + out.setSerializeNulls(oldSerializeNulls); + } + } + + @Override public DoubleContainer read(JsonReader in) throws IOException { + throw new AssertionError("not used by this test"); + } + }) + .create(); + + Map map = new HashMap<>(); + map.put(new DoubleContainer(null), 1); + + // Use TypeAdapter to avoid default lenientness of Gson + TypeAdapter> adapter = gson.getAdapter(new TypeToken>() {}); + + String expectedJson = "[[{\"c1\":NaN,\"c2\":null},1]]"; + + // First create a permissive writer + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setSerializeNulls(true); + jsonWriter.setLenient(true); + + adapter.write(jsonWriter, map); + assertEquals(expectedJson, writer.toString()); + + // Should still have original settings values + assertEquals(true, jsonWriter.getSerializeNulls()); + assertEquals(true, jsonWriter.isLenient()); + + // Then try non-permissive writer; should have same result because custom + // adapter temporarily changed writer settings + writer = new StringWriter(); + jsonWriter = new JsonWriter(writer); + jsonWriter.setSerializeNulls(false); + jsonWriter.setLenient(false); + + adapter.write(jsonWriter, map); + assertEquals(expectedJson, writer.toString()); + + // Should still have original settings values + assertEquals(false, jsonWriter.getSerializeNulls()); + assertEquals(false, jsonWriter.isLenient()); + } + static class DoubleContainer { Double d = Double.NaN; From b92d42e40f5fedb28ff7982932ecced8c075048e Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 24 Jul 2022 21:00:27 +0200 Subject: [PATCH 3/5] Extend RuntimeTypeAdapterFactoryTest --- .../RuntimeTypeAdapterFactoryTest.java | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java index 9af7d68822..e996232f26 100644 --- a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java +++ b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java @@ -21,6 +21,7 @@ import com.google.gson.JsonParseException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; @@ -217,10 +218,87 @@ static class BankTransfer extends BillingInstrument { } } + @JsonAdapter(CustomClass.Adapter.class) + static class CustomClass extends DummyBaseClass { + Boolean hasTypeField = null; + int f; + + CustomClass(int f) { + this.f = f; + } + + static class Adapter extends TypeAdapter { + @Override public void write(JsonWriter out, CustomClass value) throws IOException { + out.beginObject(); + out.name("f"); + out.value(value.f); + out.endObject(); + } + + @Override public CustomClass read(JsonReader in) throws IOException { + Boolean hasTypeField = null; + Integer fieldValue = null; + + in.beginObject(); + while (in.hasNext()) { + String name = in.nextName(); + if (name.equals("t")) { + assertNull(hasTypeField); + hasTypeField = true; + assertEquals(in.nextString(), "custom-name"); + } else if (name.equals("f")) { + assertNull(fieldValue); + fieldValue = in.nextInt(); + } else { + fail("Unexpected name: " + name); + } + } + in.endObject(); + + assertNotNull(fieldValue); + + CustomClass result = new CustomClass(fieldValue); + // Compare with Boolean.TRUE because value might be null + result.hasTypeField = Boolean.TRUE.equals(hasTypeField); + return result; + } + } + } + + public void testCustomTypeFieldName() { + TypeAdapterFactory factory = RuntimeTypeAdapterFactory.of(DummyBaseClass.class, "t") + .registerSubtype(CustomClass.class, "custom-name"); + Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(factory) + .create(); + + assertEquals("{\"t\":\"custom-name\",\"f\":1}", gson.toJson(new CustomClass(1))); + + CustomClass deserialized = (CustomClass) gson.fromJson("{\"t\":\"custom-name\",\"f\":1}", DummyBaseClass.class); + // Type field should have been removed + assertFalse(deserialized.hasTypeField); + assertEquals(1, deserialized.f); + } + + public void testMaintainType() { + TypeAdapterFactory factory = RuntimeTypeAdapterFactory.of(DummyBaseClass.class, "t", true) + .registerSubtype(CustomClass.class, "custom-name"); + Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(factory) + .create(); + + assertEquals("{\"f\":1}", gson.toJson(new CustomClass(1))); + + CustomClass deserialized = (CustomClass) gson.fromJson("{\"t\":\"custom-name\",\"f\":1}", DummyBaseClass.class); + // Type field should not have been removed, and type adapter should have seen it + assertTrue(deserialized.hasTypeField); + assertEquals(1, deserialized.f); + } + public void testDeserializeReaderSettings() throws IOException { Gson gson = new GsonBuilder() .registerTypeAdapterFactory(RuntimeTypeAdapterFactory - .of(DummyBaseClass.class, "type", true).registerSubtype(DoubleContainer.class, "d")) + .of(DummyBaseClass.class, "type").registerSubtype(DoubleContainer.class, "d")) .create(); // Use TypeAdapter to avoid default lenientness of Gson TypeAdapter adapter = gson.getAdapter(DummyBaseClass.class); From c57c5f473e03d12f09b248de5da4b4ffa66e5a45 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 24 Jul 2022 21:21:34 +0200 Subject: [PATCH 4/5] Improve comments and javadoc --- .../typeadapters/RuntimeTypeAdapterFactory.java | 5 +++-- .../main/java/com/google/gson/TypeAdapter.java | 16 +++++++++------- .../gson/internal/bind/JsonTreeReader.java | 3 +++ .../internal/bind/MapTypeAdapterFactory.java | 5 +++-- .../java/com/google/gson/stream/JsonWriter.java | 3 ++- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index ad64a9c517..2e15c6197b 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -282,8 +282,9 @@ private void writeObjectPermissively(JsonWriter out, JsonObject object) throws I * JsonObject here, make it as permissive as possible. * * This has no effect if adapter did not change settings. Then JsonObject was written - * with same settings as `out` and the following temporary setting changes won't make - * a difference. + * with same settings as `out` and the following temporary settings changes won't make + * a difference (assuming JsonTreeWriter and JsonWriter both handle the settings in the + * same way). * * Unfortunately this workaround won't work for HTML-safe and indentation settings, * though at least they do not affect the JSON data, only the formatting. diff --git a/gson/src/main/java/com/google/gson/TypeAdapter.java b/gson/src/main/java/com/google/gson/TypeAdapter.java index 3bacab3883..07f137c7c4 100644 --- a/gson/src/main/java/com/google/gson/TypeAdapter.java +++ b/gson/src/main/java/com/google/gson/TypeAdapter.java @@ -245,10 +245,15 @@ public final JsonElement toJsonTree(T value) { * {@linkplain JsonWriter#setLenient(boolean) lenient mode} applied from * the given writer. * - *

Note: In case the result of this method is afterwards written + *

Note: The {@link #write(JsonWriter, Object)} implementation of this + * type adapter might temporarily change the settings of the internally + * used writer during serialization, for example to make it lenient. In + * case the {@code JsonElement} result of this method is afterwards written * to {@code otherWriter}, possibly after some modifications to the * {@code JsonElement}, it might be necessary to temporarily reconfigure - * {@code otherWriter}: + * {@code otherWriter} to make sure the result is fully preserved (e.g. + * no fields with {@code null} value are omitted) and no exceptions are + * thrown due to lenient mode mismatch: *

{@code
    *boolean oldLenient = otherWriter.isLenient();
    *boolean oldSerializeNulls = otherWriter.getSerializeNulls();
@@ -263,12 +268,9 @@ public final JsonElement toJsonTree(T value) {
    *  otherWriter.setSerializeNulls(oldSerializeNulls);
    *}
    * }
- * This is necessary because this type adapter might have temporarily - * changed the settings of the internally used writer during serialization, - * for example to make it lenient. * * @param value the Java object to convert. May be null. - * @param otherWriter whose settings should be used for serialization + * @param otherWriter whose settings should be used for serialization. * @return the converted JSON tree. May be {@link JsonNull}. */ public final JsonElement toJsonTreeWithSettingsFrom(T value, JsonWriter otherWriter) { @@ -346,7 +348,7 @@ public final T fromJsonTree(JsonElement jsonTree) { * the given reader. * * @param jsonTree the JSON element to convert. May be {@link JsonNull}. - * @param otherReader whose settings should be used for deserialization + * @param otherReader whose settings should be used for deserialization. * @return the converted Java object. May be null. */ public final T fromJsonTreeWithSettingsFrom(JsonElement jsonTree, JsonReader otherReader) { diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java index da31bb474f..2ef70417b2 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java @@ -68,6 +68,9 @@ public JsonTreeReader(JsonElement element) { push(element); } + /** + * Applies all settings relevant for {@code JsonTreeReader} from the given reader. + */ public void applySettingsFrom(JsonReader reader) { setLenient(reader.isLenient()); } diff --git a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java index d5eb873941..0690acff0e 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java @@ -232,8 +232,9 @@ public Adapter(Gson context, Type keyType, TypeAdapter keyTypeAdapter, * JsonElement here, make it as permissive as possible. * * This has no effect if adapter did not change settings. Then JsonElement was written - * with same settings as `out` and the following temporary setting changes won't make - * a difference. + * with same settings as `out` and the following temporary settings changes won't make + * a difference (assuming JsonTreeWriter and JsonWriter both handle the settings in the + * same way). * * Unfortunately this workaround won't work for HTML-safe and indentation settings, * though at least they do not affect the JSON data, only the formatting. 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 f17f4aa252..1f59e7ca30 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -258,7 +258,8 @@ public boolean isLenient() { * and XML documents. This escapes the HTML characters {@code <}, {@code >}, * {@code &} and {@code =} before writing them to the stream. Without this * setting, your XML/HTML encoder should replace these characters with the - * corresponding escape sequences. By default this writer is not HTML-safe. + * corresponding escape sequences. By default this writer does not emit + * HTML-safe JSON. */ public final void setHtmlSafe(boolean htmlSafe) { this.htmlSafe = htmlSafe; From 5615b88048aa252618ac8fddef26b83d3b1f6bab Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 24 Jul 2022 21:51:30 +0200 Subject: [PATCH 5/5] Improve tests --- .../RuntimeTypeAdapterFactoryTest.java | 48 ++++----- .../java/com/google/gson/TypeAdapterTest.java | 33 +++++-- .../functional/MapAsArrayTypeAdapterTest.java | 98 ++++++++++++------- 3 files changed, 114 insertions(+), 65 deletions(-) diff --git a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java index e996232f26..c4e592795c 100644 --- a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java +++ b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java @@ -383,31 +383,35 @@ public void testSerializeAdapterOverwriting() throws IOException { String expectedJson = "{\"type\":\"d\",\"c1\":NaN,\"c2\":null}"; // First create a permissive writer - StringWriter writer = new StringWriter(); - JsonWriter jsonWriter = new JsonWriter(writer); - jsonWriter.setSerializeNulls(true); - jsonWriter.setLenient(true); - - adapter.write(jsonWriter, new DoubleContainer(0.0)); - assertEquals(expectedJson, writer.toString()); - - // Should still have original settings values - assertEquals(true, jsonWriter.getSerializeNulls()); - assertEquals(true, jsonWriter.isLenient()); + { + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setSerializeNulls(true); + jsonWriter.setLenient(true); + + adapter.write(jsonWriter, new DoubleContainer(0.0)); + assertEquals(expectedJson, writer.toString()); + + // Should still have original settings values + assertEquals(true, jsonWriter.getSerializeNulls()); + assertEquals(true, jsonWriter.isLenient()); + } // Then try non-permissive writer; should have same result because custom // adapter temporarily changed writer settings - writer = new StringWriter(); - jsonWriter = new JsonWriter(writer); - jsonWriter.setSerializeNulls(false); - jsonWriter.setLenient(false); - - adapter.write(jsonWriter, new DoubleContainer(0.0)); - assertEquals(expectedJson, writer.toString()); - - // Should still have original settings values - assertEquals(false, jsonWriter.getSerializeNulls()); - assertEquals(false, jsonWriter.isLenient()); + { + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setSerializeNulls(false); + jsonWriter.setLenient(false); + + adapter.write(jsonWriter, new DoubleContainer(0.0)); + assertEquals(expectedJson, writer.toString()); + + // Should still have original settings values + assertEquals(false, jsonWriter.getSerializeNulls()); + assertEquals(false, jsonWriter.isLenient()); + } } static class DummyBaseClass { diff --git a/gson/src/test/java/com/google/gson/TypeAdapterTest.java b/gson/src/test/java/com/google/gson/TypeAdapterTest.java index d6fa2ca09e..90258c219f 100644 --- a/gson/src/test/java/com/google/gson/TypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/TypeAdapterTest.java @@ -68,10 +68,19 @@ public void testFromJson_String_TrailingData() throws IOException { @Test public void testToJsonTree() { - JsonObject expectedJson = new JsonObject(); - expectedJson.add("d", JsonNull.INSTANCE); + { + JsonObject expectedJson = new JsonObject(); + expectedJson.addProperty("d", 1.0); - assertEquals(expectedJson, customDoubleAdapter.toJsonTree(null)); + assertEquals(expectedJson, customDoubleAdapter.toJsonTree(1.0)); + } + + { + JsonObject expectedJson = new JsonObject(); + expectedJson.add("d", JsonNull.INSTANCE); + + assertEquals(expectedJson, customDoubleAdapter.toJsonTree(null)); + } try { customDoubleAdapter.toJsonTree(Double.NaN); @@ -85,10 +94,12 @@ public void testToJsonTree() { public void testToJsonTreeWithSettingsFrom() { JsonWriter customWriter = new JsonWriter(new StringWriter()); - JsonObject expectedJson = new JsonObject(); - expectedJson.add("d", JsonNull.INSTANCE); + { + JsonObject expectedJson = new JsonObject(); + expectedJson.add("d", JsonNull.INSTANCE); - assertEquals(expectedJson, customDoubleAdapter.toJsonTreeWithSettingsFrom(null, customWriter)); + assertEquals(expectedJson, customDoubleAdapter.toJsonTreeWithSettingsFrom(null, customWriter)); + } try { customDoubleAdapter.toJsonTreeWithSettingsFrom(Double.NaN, customWriter); @@ -103,13 +114,17 @@ public void testToJsonTreeWithSettingsFrom() { // Should be empty JSON object assertEquals(new JsonObject(), customDoubleAdapter.toJsonTreeWithSettingsFrom(null, customWriter)); - expectedJson = new JsonObject(); - expectedJson.addProperty("d", Double.NaN); - assertEquals(expectedJson, customDoubleAdapter.toJsonTreeWithSettingsFrom(Double.NaN, customWriter)); + { + JsonObject expectedJson = new JsonObject(); + expectedJson.addProperty("d", Double.NaN); + assertEquals(expectedJson, customDoubleAdapter.toJsonTreeWithSettingsFrom(Double.NaN, customWriter)); + } } @Test public void testFromJsonTree() { + assertEquals((Double) 1.0, customDoubleAdapter.fromJsonTree(new JsonPrimitive(1.0))); + try { customDoubleAdapter.fromJsonTree(new JsonPrimitive(Double.NaN)); fail(); diff --git a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java index 5c79b37b53..323a7121db 100644 --- a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java @@ -26,7 +26,7 @@ import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.Type; -import java.util.HashMap; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import junit.framework.TestCase; @@ -141,7 +141,7 @@ static class Point { } static class PointWithProperty { - Map map = new HashMap<>(); + Map map = new LinkedHashMap<>(); } /** @@ -153,19 +153,45 @@ public void testCustomJsonWriter() throws IOException { .enableComplexMapKeySerialization() .serializeSpecialFloatingPointValues() .create(); - StringWriter writer = new StringWriter(); - JsonWriter jsonWriter = new JsonWriter(writer); - jsonWriter.setLenient(true); - jsonWriter.setSerializeNulls(false); - Map map = new HashMap<>(); + // Use TypeAdapter to avoid default lenientness of Gson + TypeAdapter> adapter = gson.getAdapter(new TypeToken>() {}); + + { + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setSerializeNulls(true); + + adapter.write(jsonWriter, Collections.singletonMap(new DoubleContainer(null), 1)); + assertEquals("[[{\"d\":null},1]]", writer.toString()); + } + + + Map map = new LinkedHashMap<>(); map.put(new DoubleContainer(null), 1); map.put(new DoubleContainer(Double.NaN), 2); - // Use TypeAdapter to avoid default lenientness of Gson - TypeAdapter> adapter = gson.getAdapter(new TypeToken>() {}); - adapter.write(jsonWriter, map); - assertEquals("[[{},1],[{\"d\":NaN},2]]", writer.toString()); + { + JsonWriter jsonWriter = new JsonWriter(new StringWriter()); + jsonWriter.setLenient(false); + + try { + adapter.write(jsonWriter, map); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage()); + } + } + + { + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setLenient(true); + jsonWriter.setSerializeNulls(false); + + adapter.write(jsonWriter, map); + assertEquals("[[{},1],[{\"d\":NaN},2]]", writer.toString()); + } } /** @@ -200,7 +226,7 @@ public void testSerializeAdapterOverwriting() throws IOException { }) .create(); - Map map = new HashMap<>(); + Map map = new LinkedHashMap<>(); map.put(new DoubleContainer(null), 1); // Use TypeAdapter to avoid default lenientness of Gson @@ -209,31 +235,35 @@ public void testSerializeAdapterOverwriting() throws IOException { String expectedJson = "[[{\"c1\":NaN,\"c2\":null},1]]"; // First create a permissive writer - StringWriter writer = new StringWriter(); - JsonWriter jsonWriter = new JsonWriter(writer); - jsonWriter.setSerializeNulls(true); - jsonWriter.setLenient(true); - - adapter.write(jsonWriter, map); - assertEquals(expectedJson, writer.toString()); - - // Should still have original settings values - assertEquals(true, jsonWriter.getSerializeNulls()); - assertEquals(true, jsonWriter.isLenient()); + { + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setSerializeNulls(true); + jsonWriter.setLenient(true); + + adapter.write(jsonWriter, map); + assertEquals(expectedJson, writer.toString()); + + // Should still have original settings values + assertEquals(true, jsonWriter.getSerializeNulls()); + assertEquals(true, jsonWriter.isLenient()); + } // Then try non-permissive writer; should have same result because custom // adapter temporarily changed writer settings - writer = new StringWriter(); - jsonWriter = new JsonWriter(writer); - jsonWriter.setSerializeNulls(false); - jsonWriter.setLenient(false); - - adapter.write(jsonWriter, map); - assertEquals(expectedJson, writer.toString()); - - // Should still have original settings values - assertEquals(false, jsonWriter.getSerializeNulls()); - assertEquals(false, jsonWriter.isLenient()); + { + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setSerializeNulls(false); + jsonWriter.setLenient(false); + + adapter.write(jsonWriter, map); + assertEquals(expectedJson, writer.toString()); + + // Should still have original settings values + assertEquals(false, jsonWriter.getSerializeNulls()); + assertEquals(false, jsonWriter.isLenient()); + } } static class DoubleContainer {