From 9ad3358728073500a5fc30f9685eefa48696fe23 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 6 Jun 2011 05:40:13 +0000 Subject: [PATCH] Document RuntimeTypeAdapter --- .../gson/typeadapters/RuntimeTypeAdapter.java | 129 +++++++++++++++++- .../typeadapters/RuntimeTypeAdapterTest.java | 125 ++++++++++++++++- 2 files changed, 241 insertions(+), 13 deletions(-) diff --git a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapter.java b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapter.java index ea983613..8f399cc2 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapter.java +++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapter.java @@ -28,28 +28,136 @@ import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; -public final class RuntimeTypeAdapter - implements JsonSerializer, JsonDeserializer { - +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *
   {@code
+ *   abstract class Shape {
+ *     int x;
+ *     int y;
+ *   }
+ *   class Circle extends Shape {
+ *     int radius;
+ *   }
+ *   class Rectangle extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Diamond extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Drawing {
+ *     Shape bottomShape;
+ *     Shape topShape;
+ *   }
+ * }
+ *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * Create a {@code RuntimeTypeAdapter} by passing the base type and type field + * name to the {@link #create} factory method. If you don't supply an explicit + * type field name, {@code "type"} will be used.
   {@code
+ *   RuntimeTypeAdapter shapeAdapter
+ *       = RuntimeTypeAdapter.create(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   {@code
+ *   shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapter.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapter.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapter(Shape.class, shapeAdapter)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapter shapeAdapter = RuntimeTypeAdapter.create(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ */ +public final class RuntimeTypeAdapter implements JsonSerializer, JsonDeserializer { private final Class baseType; private final String typeFieldName; private final Map> labelToSubtype = new LinkedHashMap>(); private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); - public RuntimeTypeAdapter(Class baseType, String typeFieldName) { + private RuntimeTypeAdapter(Class baseType, String typeFieldName) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } this.baseType = baseType; this.typeFieldName = typeFieldName; } + /** + * Creates a new runtime type adapter for {@code c} using {@code "type"} as + * the type field name. + */ public static RuntimeTypeAdapter create(Class c) { - return new RuntimeTypeAdapter(c, "type"); + return create(c, "type"); } + /** + * Creates a new runtime type adapter using for {@code c} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ public static RuntimeTypeAdapter create(Class c, String typeFieldName) { return new RuntimeTypeAdapter(c, typeFieldName); } + /** + * 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 RuntimeTypeAdapter 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"); } @@ -58,6 +166,13 @@ public final class RuntimeTypeAdapter 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 RuntimeTypeAdapter registerSubtype(Class type) { return registerSubtype(type, type.getSimpleName()); } @@ -66,13 +181,13 @@ public final class RuntimeTypeAdapter Class srcType = src.getClass(); String label = subtypeToLabel.get(srcType); if (label == null) { - throw new IllegalArgumentException("cannot serialize " + srcType.getName() + throw new JsonParseException("cannot serialize " + srcType.getName() + "; did you forget to register a subtype?"); } JsonElement serialized = context.serialize(src, srcType); final JsonObject jsonObject = serialized.getAsJsonObject(); if (jsonObject.has(typeFieldName)) { - throw new IllegalArgumentException("cannot serialize " + srcType.getName() + throw new JsonParseException("cannot serialize " + srcType.getName() + " because it already defines a field named " + typeFieldName); } JsonObject clone = new JsonObject(); diff --git a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterTest.java b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterTest.java index 033ccd1d..a842ec55 100644 --- a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterTest.java +++ b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterTest.java @@ -18,6 +18,7 @@ package com.google.gson.typeadapters; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; import junit.framework.TestCase; public final class RuntimeTypeAdapterTest extends TestCase { @@ -53,20 +54,132 @@ public final class RuntimeTypeAdapterTest extends TestCase { assertEquals("Jesse", deserialized.ownerName); } + public void testNullBaseType() { + try { + RuntimeTypeAdapter.create(null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testNullTypeFieldName() { + try { + RuntimeTypeAdapter.create(BillingInstrument.class, null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testNullSubtype() { + RuntimeTypeAdapter rta = RuntimeTypeAdapter.create(BillingInstrument.class); + try { + rta.registerSubtype(null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testNullLabel() { + RuntimeTypeAdapter rta = RuntimeTypeAdapter.create(BillingInstrument.class); + try { + rta.registerSubtype(CreditCard.class, null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testDuplicateSubtype() { + RuntimeTypeAdapter rta = RuntimeTypeAdapter.create(BillingInstrument.class); + rta.registerSubtype(CreditCard.class, "CC"); + try { + rta.registerSubtype(CreditCard.class, "Visa"); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testDuplicateLabel() { + RuntimeTypeAdapter rta = RuntimeTypeAdapter.create(BillingInstrument.class); + rta.registerSubtype(CreditCard.class, "CC"); + try { + rta.registerSubtype(BankTransfer.class, "CC"); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testDeserializeMissingTypeField() { + Object billingAdapter = RuntimeTypeAdapter.create(BillingInstrument.class) + .registerSubtype(CreditCard.class); + Gson gson = new GsonBuilder() + .registerTypeAdapter(BillingInstrument.class, billingAdapter) + .create(); + try { + gson.fromJson("{ownerName:'Jesse'}", BillingInstrument.class); + fail(); + } catch (JsonParseException expected) { + } + } + + public void testDeserializeMissingSubtype() { + Object billingAdapter = RuntimeTypeAdapter.create(BillingInstrument.class) + .registerSubtype(BankTransfer.class); + Gson gson = new GsonBuilder() + .registerTypeAdapter(BillingInstrument.class, billingAdapter) + .create(); + try { + gson.fromJson("{type:'CreditCard',ownerName:'Jesse'}", BillingInstrument.class); + fail(); + } catch (JsonParseException expected) { + } + } + + public void testSerializeMissingSubtype() { + Object billingAdapter = RuntimeTypeAdapter.create(BillingInstrument.class) + .registerSubtype(BankTransfer.class); + Gson gson = new GsonBuilder() + .registerTypeAdapter(BillingInstrument.class, billingAdapter) + .create(); + try { + gson.toJson(new CreditCard("Jesse", 456), BillingInstrument.class); + fail(); + } catch (JsonParseException expected) { + } + } + + public void testSerializeCollidingTypeFieldName() { + Object billingAdapter = RuntimeTypeAdapter.create(BillingInstrument.class, "cvv") + .registerSubtype(CreditCard.class); + Gson gson = new GsonBuilder() + .registerTypeAdapter(BillingInstrument.class, billingAdapter) + .create(); + try { + gson.toJson(new CreditCard("Jesse", 456), BillingInstrument.class); + fail(); + } catch (JsonParseException expected) { + } + } + + static class BillingInstrument { + private final String ownerName; + BillingInstrument(String ownerName) { + this.ownerName = ownerName; + } + } + static class CreditCard extends BillingInstrument { int cvv; - CreditCard(String ownerName, int cvv) { super(ownerName); this.cvv = cvv; } } - static class BillingInstrument { - private final String ownerName; - - BillingInstrument(String ownerName) { - this.ownerName = ownerName; + static class BankTransfer extends BillingInstrument { + int bankAccount; + BankTransfer(String ownerName, int bankAccount) { + super(ownerName); + this.bankAccount = bankAccount; } } }