From ee70912bb0004cb70d4631ae42f43ef95faf6eeb Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 22 May 2022 16:58:16 +0200 Subject: [PATCH 1/4] Improve Gson and JsonParser trailing data handling --- gson/src/main/java/com/google/gson/Gson.java | 61 ++++---- .../main/java/com/google/gson/JsonParser.java | 47 +++---- .../com/google/gson/JsonStreamParser.java | 8 +- .../com/google/gson/internal/Streams.java | 14 +- .../gson/internal/bind/TreeTypeAdapter.java | 4 +- .../test/java/com/google/gson/GsonTest.java | 56 +++++++- .../java/com/google/gson/JsonParserTest.java | 132 +++++++++++++----- ...onParserTest.java => GsonParsingTest.java} | 8 +- ...ntimeTypeAdapterFactoryFunctionalTest.java | 2 +- .../gson/stream/JsonReaderPathTest.java | 2 +- 10 files changed, 223 insertions(+), 111 deletions(-) rename gson/src/test/java/com/google/gson/functional/{JsonParserTest.java => GsonParsingTest.java} (97%) diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 666e5f8bd3..735916d72c 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -38,7 +38,6 @@ import com.google.gson.stream.JsonReader; 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; @@ -983,8 +982,7 @@ public T fromJson(String json, Type typeOfT) throws JsonSyntaxException { */ public T fromJson(Reader json, Class classOfT) throws JsonSyntaxException, JsonIOException { JsonReader jsonReader = newJsonReader(json); - Object object = fromJson(jsonReader, classOfT); - assertFullConsumption(object, jsonReader); + Object object = fromJson(jsonReader, classOfT, true); return Primitives.wrap(classOfT).cast(object); } @@ -1013,49 +1011,29 @@ public T fromJson(Reader json, Class classOfT) throws JsonSyntaxException @SuppressWarnings("unchecked") public T fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyntaxException { JsonReader jsonReader = newJsonReader(json); - T object = (T) fromJson(jsonReader, typeOfT); - assertFullConsumption(object, jsonReader); + T object = (T) fromJson(jsonReader, typeOfT, true); return object; } - private static void assertFullConsumption(Object obj, JsonReader reader) { - try { - if (obj != null && reader.peek() != JsonToken.END_DOCUMENT) { - throw new JsonSyntaxException("JSON document was not fully consumed."); - } - } catch (MalformedJsonException e) { - throw new JsonSyntaxException(e); - } catch (IOException e) { - throw new JsonIOException(e); - } - } - /** - * 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. - * - *

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. - * - *

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 + * @param requireEndDocument whether there must not be any trailing data after + * the first read JSON element */ - @SuppressWarnings("unchecked") - public T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException { + private T fromJson(JsonReader reader, Type typeOfT, boolean requireEndDocument) throws JsonIOException, JsonSyntaxException { boolean isEmpty = true; boolean oldLenient = reader.isLenient(); reader.setLenient(true); try { reader.peek(); isEmpty = false; + @SuppressWarnings("unchecked") // this is not actually safe TypeToken typeToken = (TypeToken) TypeToken.get(typeOfT); TypeAdapter typeAdapter = getAdapter(typeToken); T object = typeAdapter.read(reader); + + if (requireEndDocument && reader.peek() != JsonToken.END_DOCUMENT) { + throw new JsonSyntaxException("JSON document was not fully consumed."); + } return object; } catch (EOFException e) { /* @@ -1080,6 +1058,25 @@ public T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, J } } + /** + * 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. + * + *

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. + * + *

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 + */ + public T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException { + return fromJson(reader, typeOfT, false); + } + /** * This method deserializes the Json read from the specified parse tree into an object of the * specified type. It is not suitable to use if the specified class is a generic type since it diff --git a/gson/src/main/java/com/google/gson/JsonParser.java b/gson/src/main/java/com/google/gson/JsonParser.java index d3508c1073..adb3ca0183 100644 --- a/gson/src/main/java/com/google/gson/JsonParser.java +++ b/gson/src/main/java/com/google/gson/JsonParser.java @@ -17,9 +17,6 @@ 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; @@ -46,7 +43,7 @@ public JsonParser() {} * @return a parse tree of {@link JsonElement}s corresponding to the specified JSON * @throws JsonParseException if the specified text is not valid JSON */ - public static JsonElement parseString(String json) throws JsonSyntaxException { + public static JsonElement parseString(String json) throws JsonParseException { return parseReader(new StringReader(json)); } @@ -62,20 +59,25 @@ public static JsonElement parseString(String json) throws JsonSyntaxException { * @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 { + public static JsonElement parseReader(Reader reader) throws JsonParseException { + JsonReader jsonReader = new JsonReader(reader); + return parseReader(jsonReader, true); + } + + private static JsonElement parseReader(JsonReader reader, boolean requiredEndDocument) + throws JsonParseException { + boolean lenient = reader.isLenient(); + reader.setLenient(true); try { - JsonReader jsonReader = new JsonReader(reader); - JsonElement element = parseReader(jsonReader); - if (!element.isJsonNull() && jsonReader.peek() != JsonToken.END_DOCUMENT) { - throw new JsonSyntaxException("Did not consume the entire document."); - } - return element; - } catch (MalformedJsonException e) { - throw new JsonSyntaxException(e); - } catch (IOException e) { - throw new JsonIOException(e); + return Streams.parse(reader, requiredEndDocument); } catch (NumberFormatException e) { throw new JsonSyntaxException(e); + } catch (StackOverflowError e) { + throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e); + } catch (OutOfMemoryError e) { + throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e); + } finally { + reader.setLenient(lenient); } } @@ -91,19 +93,8 @@ public static JsonElement parseReader(Reader reader) throws JsonIOException, Jso * @throws JsonParseException if there is an IOException or if the specified * text is not valid JSON */ - public static JsonElement parseReader(JsonReader reader) - throws JsonIOException, JsonSyntaxException { - boolean lenient = reader.isLenient(); - reader.setLenient(true); - try { - return Streams.parse(reader); - } catch (StackOverflowError e) { - throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e); - } catch (OutOfMemoryError e) { - throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e); - } finally { - reader.setLenient(lenient); - } + public static JsonElement parseReader(JsonReader reader) throws JsonParseException { + return parseReader(reader, false); } /** @deprecated Use {@link JsonParser#parseString} */ diff --git a/gson/src/main/java/com/google/gson/JsonStreamParser.java b/gson/src/main/java/com/google/gson/JsonStreamParser.java index 27597da652..0ed9ed54c8 100644 --- a/gson/src/main/java/com/google/gson/JsonStreamParser.java +++ b/gson/src/main/java/com/google/gson/JsonStreamParser.java @@ -61,7 +61,7 @@ public final class JsonStreamParser implements Iterator { public JsonStreamParser(String json) { this(new StringReader(json)); } - + /** * @param reader The data stream containing JSON elements concatenated to each other. * @since 1.4 @@ -71,7 +71,7 @@ public JsonStreamParser(Reader reader) { parser.setLenient(true); lock = new Object(); } - + /** * Returns the next available {@link JsonElement} on the reader. Throws a * {@link NoSuchElementException} if no element is available. @@ -86,9 +86,9 @@ public JsonElement next() throws JsonParseException { if (!hasNext()) { throw new NoSuchElementException(); } - + try { - return Streams.parse(parser); + return Streams.parse(parser, false); } catch (StackOverflowError e) { throw new JsonParseException("Failed parsing JSON source to Json", e); } catch (OutOfMemoryError e) { diff --git a/gson/src/main/java/com/google/gson/internal/Streams.java b/gson/src/main/java/com/google/gson/internal/Streams.java index 0bb73aa18e..33baaa0364 100644 --- a/gson/src/main/java/com/google/gson/internal/Streams.java +++ b/gson/src/main/java/com/google/gson/internal/Streams.java @@ -23,6 +23,7 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.internal.bind.TypeAdapters; import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import com.google.gson.stream.MalformedJsonException; import java.io.EOFException; @@ -39,13 +40,22 @@ private Streams() { /** * Takes a reader in any state and returns the next value as a JsonElement. + * + * @param requireEndDocument whether there must not be any trailing data after + * the JsonElement */ - public static JsonElement parse(JsonReader reader) throws JsonParseException { + public static JsonElement parse(JsonReader reader, boolean requireEndDocument) throws JsonParseException { boolean isEmpty = true; try { reader.peek(); isEmpty = false; - return TypeAdapters.JSON_ELEMENT.read(reader); + JsonElement element = TypeAdapters.JSON_ELEMENT.read(reader); + + if (requireEndDocument && reader.peek() != JsonToken.END_DOCUMENT) { + throw new JsonSyntaxException("Did not consume the entire document."); + } + return element; + } catch (EOFException e) { /* * For compatibility with JSON 1.5 and earlier, we return a JsonNull for diff --git a/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java index 50f46b5aad..c1c0168c3d 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java @@ -62,7 +62,7 @@ public TreeTypeAdapter(JsonSerializer serializer, JsonDeserializer deseria if (deserializer == null) { return delegate().read(in); } - JsonElement value = Streams.parse(in); + JsonElement value = Streams.parse(in, false); if (value.isJsonNull()) { return null; } @@ -162,5 +162,5 @@ private final class GsonContextImpl implements JsonSerializationContext, JsonDes @Override public R deserialize(JsonElement json, Type typeOfT) throws JsonParseException { return (R) gson.fromJson(json, typeOfT); } - }; + } } diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index abb0de2113..21ffbb7526 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -16,11 +16,15 @@ package com.google.gson; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + import com.google.gson.internal.Excluder; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.google.gson.stream.MalformedJsonException; import java.io.IOException; +import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.lang.reflect.Field; @@ -29,14 +33,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import junit.framework.TestCase; +import org.junit.Test; /** * Unit tests for {@link Gson}. * * @author Ryan Harter */ -public final class GsonTest extends TestCase { +public final class GsonTest { private static final Excluder CUSTOM_EXCLUDER = Excluder.DEFAULT .excludeFieldsWithoutExposeAnnotation() @@ -51,6 +55,7 @@ public final class GsonTest extends TestCase { private static final ToNumberStrategy CUSTOM_OBJECT_TO_NUMBER_STRATEGY = ToNumberPolicy.DOUBLE; private static final ToNumberStrategy CUSTOM_NUMBER_TO_NUMBER_STRATEGY = ToNumberPolicy.LAZILY_PARSED_NUMBER; + @Test public void testOverridesDefaultExcluder() { Gson gson = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, new HashMap>(), true, false, true, false, @@ -66,6 +71,7 @@ public void testOverridesDefaultExcluder() { assertEquals(false, gson.htmlSafe()); } + @Test public void testClonedTypeAdapterFactoryListsAreIndependent() { Gson original = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, new HashMap>(), true, false, true, false, @@ -89,6 +95,7 @@ private static final class TestTypeAdapter extends TypeAdapter { @Override public Object read(JsonReader in) throws IOException { return null; } } + @Test public void testNewJsonWriter_Default() throws IOException { StringWriter writer = new StringWriter(); JsonWriter jsonWriter = new Gson().newJsonWriter(writer); @@ -111,6 +118,7 @@ public void testNewJsonWriter_Default() throws IOException { assertEquals("{\"\\u003ctest2\":true}", writer.toString()); } + @Test public void testNewJsonWriter_Custom() throws IOException { StringWriter writer = new StringWriter(); JsonWriter jsonWriter = new GsonBuilder() @@ -135,6 +143,7 @@ public void testNewJsonWriter_Custom() throws IOException { assertEquals(")]}'\n{\n \"test\": null,\n \" registerSubtype(Class type) { return new TypeAdapter() { @Override public R read(JsonReader in) throws IOException { - JsonElement jsonElement = Streams.parse(in); + JsonElement jsonElement = Streams.parse(in, false); JsonElement labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); if (labelJsonElement == null) { throw new JsonParseException("cannot deserialize " + baseType diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java index ab802be1d7..87e013ee8a 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java @@ -315,7 +315,7 @@ public enum Factory { }, OBJECT_READER { @Override public JsonReader create(String data) { - JsonElement element = Streams.parse(new JsonReader(new StringReader(data))); + JsonElement element = Streams.parse(new JsonReader(new StringReader(data)), false); return new JsonTreeReader(element); } }; From 0938138481848006f6dd2b1423c617bfe7d09325 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 23 Jul 2022 17:40:54 +0200 Subject: [PATCH 2/4] Add missing Test annotations for JsonParserTest --- gson/src/test/java/com/google/gson/JsonParserTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gson/src/test/java/com/google/gson/JsonParserTest.java b/gson/src/test/java/com/google/gson/JsonParserTest.java index 3fd2760be9..7ca8f508a2 100644 --- a/gson/src/test/java/com/google/gson/JsonParserTest.java +++ b/gson/src/test/java/com/google/gson/JsonParserTest.java @@ -190,6 +190,7 @@ private static String repeat(String s, int times) { } /** Deeply nested JSON arrays should not cause {@link StackOverflowError} */ + @Test public void testParseDeeplyNestedArrays() throws IOException { int times = 10000; // [[[ ... ]]] @@ -209,6 +210,7 @@ public void testParseDeeplyNestedArrays() throws IOException { } /** Deeply nested JSON objects should not cause {@link StackOverflowError} */ + @Test public void testParseDeeplyNestedObjects() throws IOException { int times = 10000; // {"a":{"a": ... {"a":null} ... }} From 4ab2af9c4110758c56ee88f34dcc7abd5744ebe6 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 23 Jul 2022 17:52:48 +0200 Subject: [PATCH 3/4] Add back Streams.parse(JsonReader) --- .../java/com/google/gson/internal/Streams.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gson/src/main/java/com/google/gson/internal/Streams.java b/gson/src/main/java/com/google/gson/internal/Streams.java index 33baaa0364..83c832951f 100644 --- a/gson/src/main/java/com/google/gson/internal/Streams.java +++ b/gson/src/main/java/com/google/gson/internal/Streams.java @@ -20,6 +20,7 @@ import com.google.gson.JsonIOException; import com.google.gson.JsonNull; import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import com.google.gson.internal.bind.TypeAdapters; import com.google.gson.stream.JsonReader; @@ -38,6 +39,22 @@ private Streams() { throw new UnsupportedOperationException(); } + /** + * @deprecated + * This method is declared in an internal Gson class. Use the public API class {@link JsonParser} + * (note that {@code JsonParser} parses JSON in lenient mode), or obtain the {@code TypeAdapter} + * for {@code JsonElement} and use that for parsing: + *
{@code
+   *TypeAdapter adapter = gson.getAdapter(JsonElement.class);
+   *JsonElement element = adapter.read(...);
+   *    }
+ */ + // Only keeping this internal method because third-party projects depend on it + @Deprecated + public static JsonElement parse(JsonReader reader) { + return parse(reader, false); + } + /** * Takes a reader in any state and returns the next value as a JsonElement. * From d4fc47024d77bd39bb8b5def78bba9194607c48f Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 23 Jul 2022 18:11:35 +0200 Subject: [PATCH 4/4] Move RuntimeTypeAdapterFactoryFunctionalTest to extras module --- .../RuntimeTypeAdapterFactory.java | 8 +- .../RuntimeTypeAdapterFactoryTest.java | 57 ++++- ...ntimeTypeAdapterFactoryFunctionalTest.java | 203 ------------------ 3 files changed, 60 insertions(+), 208 deletions(-) delete mode 100644 gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java 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..64bd26ea33 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -156,7 +156,7 @@ private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boole 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. @@ -179,6 +179,7 @@ public static RuntimeTypeAdapterFactory of(Class baseType) { * * @throws IllegalArgumentException if either {@code type} or {@code label} * have already been registered on this type adapter. + * @return this */ public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { if (type == null || label == null) { @@ -198,6 +199,7 @@ public RuntimeTypeAdapterFactory registerSubtype(Class type, Str * * @throws IllegalArgumentException if either {@code type} or its simple name * have already been registered on this type adapter. + * @return this */ public RuntimeTypeAdapterFactory registerSubtype(Class type) { return registerSubtype(type, type.getSimpleName()); @@ -227,7 +229,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); @@ -265,7 +267,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/typeadapters/RuntimeTypeAdapterFactoryTest.java b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java index e58ee0f9c3..fab91ed3ce 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,10 @@ 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.annotations.JsonAdapter; +import com.google.gson.reflect.TypeToken; import junit.framework.TestCase; public final class RuntimeTypeAdapterFactoryTest extends TestCase { @@ -33,6 +36,9 @@ public void testRuntimeTypeAdapter() { .create(); CreditCard original = new CreditCard("Jesse", 234); + assertEquals("{\"type\":\"CreditCard\",\"cvv\":234,\"ownerName\":\"Jesse\"}", + //explicitly specify the base class as serialized class + gson.toJson(original, BillingInstrument.class)); assertEquals("{\"type\":\"CreditCard\",\"cvv\":234,\"ownerName\":\"Jesse\"}", //do not give the explicit typeOfSrc, because if this would be in a list //or an attribute, there would also be no hint. See #712 @@ -172,10 +178,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 +216,51 @@ static class BankTransfer extends BillingInstrument { this.bankAccount = bankAccount; } } + + public void testJsonAdapterDelegate() throws Exception { + Gson gson = new Gson(); + Shape shape = new Circle(25); + String json = gson.toJson(shape); + assertEquals("{\"radius\":25,\"type\":\"CIRCLE\"}", json); + shape = gson.fromJson(json, Shape.class); + assertEquals(25, ((Circle)shape).radius); + + shape = new Square(15); + json = gson.toJson(shape); + assertEquals("{\"side\":15,\"type\":\"SQUARE\"}", json); + shape = gson.fromJson(json, Shape.class); + assertEquals(15, ((Square)shape).side); + assertEquals(ShapeType.SQUARE, shape.type); + } + + @JsonAdapter(Shape.JsonAdapterFactory.class) + static class Shape { + final ShapeType type; + Shape(ShapeType type) { this.type = type; } + + private static final class JsonAdapterFactory implements TypeAdapterFactory { + private static final RuntimeTypeAdapterFactory delegate = RuntimeTypeAdapterFactory.of(Shape.class, "type", true) + .registerSubtype(Circle.class, ShapeType.CIRCLE.toString()) + .registerSubtype(Square.class, ShapeType.SQUARE.toString()); + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + return delegate.create(gson, type); + } + } + } + + enum ShapeType { + SQUARE, CIRCLE + } + + private static final class Circle extends Shape { + final int radius; + Circle(int radius) { super(ShapeType.CIRCLE); this.radius = radius; } + } + + private static final class Square extends Shape { + final int side; + Square(int side) { super(ShapeType.SQUARE); this.side = side; } + } } diff --git a/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java b/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java deleted file mode 100644 index 33bce48311..0000000000 --- a/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (C) 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.gson.functional; - -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -import junit.framework.TestCase; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.annotations.JsonAdapter; -import com.google.gson.internal.Streams; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -/** - * Functional tests for the RuntimeTypeAdapterFactory feature in extras. - */ -public final class RuntimeTypeAdapterFactoryFunctionalTest extends TestCase { - - private final Gson gson = new Gson(); - - /** - * This test also ensures that {@link TypeAdapterFactory} registered through {@link JsonAdapter} - * work correctly for {@link Gson#getDelegateAdapter(TypeAdapterFactory, TypeToken)}. - */ - public void testSubclassesAutomaticallySerialized() throws Exception { - Shape shape = new Circle(25); - String json = gson.toJson(shape); - shape = gson.fromJson(json, Shape.class); - assertEquals(25, ((Circle)shape).radius); - - shape = new Square(15); - json = gson.toJson(shape); - shape = gson.fromJson(json, Shape.class); - assertEquals(15, ((Square)shape).side); - assertEquals(ShapeType.SQUARE, shape.type); - } - - @JsonAdapter(Shape.JsonAdapterFactory.class) - static class Shape { - final ShapeType type; - Shape(ShapeType type) { this.type = type; } - private static final class JsonAdapterFactory extends RuntimeTypeAdapterFactory { - public JsonAdapterFactory() { - super(Shape.class, "type"); - registerSubtype(Circle.class, ShapeType.CIRCLE.toString()); - registerSubtype(Square.class, ShapeType.SQUARE.toString()); - } - } - } - - public enum ShapeType { - SQUARE, CIRCLE - } - - private static final class Circle extends Shape { - final int radius; - Circle(int radius) { super(ShapeType.CIRCLE); this.radius = radius; } - } - - private static final class Square extends Shape { - final int side; - Square(int side) { super(ShapeType.SQUARE); this.side = side; } - } - - // Copied from the extras package - static class RuntimeTypeAdapterFactory implements TypeAdapterFactory { - private final Class baseType; - private final String typeFieldName; - private final Map> labelToSubtype = new LinkedHashMap<>(); - private final Map, String> subtypeToLabel = new LinkedHashMap<>(); - - protected RuntimeTypeAdapterFactory(Class baseType, String typeFieldName) { - if (typeFieldName == null || baseType == null) { - throw new NullPointerException(); - } - this.baseType = baseType; - this.typeFieldName = typeFieldName; - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { - return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName); - } - - /** - * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as - * the type field name. - */ - public static RuntimeTypeAdapterFactory of(Class baseType) { - return new RuntimeTypeAdapterFactory<>(baseType, "type"); - } - - /** - * Registers {@code type} identified by {@code label}. Labels are case - * sensitive. - * - * @throws IllegalArgumentException if either {@code type} or {@code label} - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { - if (type == null || label == null) { - throw new NullPointerException(); - } - if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { - throw new IllegalArgumentException("types and labels must be unique"); - } - labelToSubtype.put(label, type); - subtypeToLabel.put(type, label); - return this; - } - - /** - * Registers {@code type} identified by its {@link Class#getSimpleName simple - * name}. Labels are case sensitive. - * - * @throws IllegalArgumentException if either {@code type} or its simple name - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type) { - return registerSubtype(type, type.getSimpleName()); - } - - @Override public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() != baseType) { - return null; - } - - final Map> labelToDelegate = new LinkedHashMap<>(); - final Map, TypeAdapter> subtypeToDelegate = new LinkedHashMap<>(); - for (Map.Entry> entry : labelToSubtype.entrySet()) { - TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); - labelToDelegate.put(entry.getKey(), delegate); - subtypeToDelegate.put(entry.getValue(), delegate); - } - - return new TypeAdapter() { - @Override public R read(JsonReader in) throws IOException { - JsonElement jsonElement = Streams.parse(in, false); - JsonElement labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); - if (labelJsonElement == null) { - throw new JsonParseException("cannot deserialize " + baseType - + " because it does not define a field named " + typeFieldName); - } - String label = labelJsonElement.getAsString(); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); - if (delegate == null) { - throw new JsonParseException("cannot deserialize " + baseType + " subtype named " - + label + "; did you forget to register a subtype?"); - } - return delegate.fromJsonTree(jsonElement); - } - - @Override public void write(JsonWriter out, R value) throws IOException { - Class srcType = value.getClass(); - String label = subtypeToLabel.get(srcType); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); - if (delegate == null) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + "; did you forget to register a subtype?"); - } - JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); - if (!jsonObject.has(typeFieldName)) { - JsonObject clone = new JsonObject(); - clone.add(typeFieldName, new JsonPrimitive(label)); - for (Map.Entry e : jsonObject.entrySet()) { - clone.add(e.getKey(), e.getValue()); - } - jsonObject = clone; - } - Streams.write(jsonObject, out); - } - }; - } - } -}