From 12161afb0178dd1d49ef8443fb6b1e1cd152d8db Mon Sep 17 00:00:00 2001 From: Ville Koskela Date: Mon, 16 Oct 2017 14:29:51 -0700 Subject: [PATCH] Extend support for deserializing via builder if the builder has type bindings by inferring those bindings from the value type. This behavior is not ideal, but addresses the common case, and is controlled by a MapperFeature. --- .../jackson/databind/MapperFeature.java | 12 +++++++ .../deser/BeanDeserializerFactory.java | 14 ++++++--- .../jackson/databind/type/TypeFactory.java | 24 +++++++++++++- .../BuilderWithTypeParametersTest.java} | 31 ++++++++++++++----- .../databind/type/TestTypeFactory.java | 16 +++++++++- 5 files changed, 83 insertions(+), 14 deletions(-) rename src/test/java/com/fasterxml/jackson/{failing/BuilderDeserializationTest921.java => databind/deser/builder/BuilderWithTypeParametersTest.java} (70%) diff --git a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java index ab64e26e29..ecbda48b81 100644 --- a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java @@ -198,6 +198,18 @@ public enum MapperFeature implements ConfigFeature */ USE_STATIC_TYPING(false), + /** + * Feature that enables inferring builder type bindings from the value type + * being deserialized. This requires that the generic type declaration on + * the value type match that on the builder exactly. + *

+ * Feature is disabled by default which means that deserialization does + * not support deserializing types via builders with type parameters. + *

+ * See: https://github.com/FasterXML/jackson-databind/issues/921 + */ + INFER_BUILDER_TYPE_BINDINGS(false), + /* /****************************************************** /* View-related features diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index d21932bf9c..0afab68d9a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -138,12 +138,18 @@ public JsonDeserializer createBeanDeserializer(DeserializationContext ct } @Override - public JsonDeserializer createBuilderBasedDeserializer(DeserializationContext ctxt, - JavaType valueType, BeanDescription beanDesc, Class builderClass) - throws JsonMappingException + public JsonDeserializer createBuilderBasedDeserializer( + DeserializationContext ctxt, JavaType valueType, BeanDescription beanDesc, + Class builderClass) + throws JsonMappingException { // First: need a BeanDescription for builder class - JavaType builderType = ctxt.constructType(builderClass); + JavaType builderType; + if (ctxt.isEnabled(MapperFeature.INFER_BUILDER_TYPE_BINDINGS)) { + builderType = ctxt.getTypeFactory().constructParametricType(builderClass, valueType.getBindings()); + } else { + builderType = ctxt.constructType(builderClass); + } BeanDescription builderDesc = ctxt.getConfig().introspectForBuilder(builderType); return buildBuilderBasedDeserializer(ctxt, valueType, builderDesc); } diff --git a/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java b/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java index 766cb69110..e65eee006a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java @@ -894,7 +894,29 @@ public JavaType constructParametricType(Class parametrized, Class... param */ public JavaType constructParametricType(Class rawType, JavaType... parameterTypes) { - return _fromClass(null, rawType, TypeBindings.create(rawType, parameterTypes)); + return constructParametricType(rawType, TypeBindings.create(rawType, parameterTypes)); + } + + /** + * Factory method for constructing {@link JavaType} that + * represents a parameterized type. The type's parameters are + * specified as an instance of {@link TypeBindings}. This + * is useful if you already have the type's parameters such + * as those found on {@link JavaType}. For example, you could + * call + *
+     *   return TypeFactory.constructParametricType(ArrayList.class, javaType.getBindings());
+     * 
+ * This effectively applies the parameterized types from one + * {@link JavaType} to another class. + * + * @param rawType Actual type-erased type + * @param parameterTypes Type bindings for the raw type + * @since 3.0 + */ + public JavaType constructParametricType(Class rawType, TypeBindings parameterTypes) + { + return _fromClass(null, rawType, parameterTypes); } /* diff --git a/src/test/java/com/fasterxml/jackson/failing/BuilderDeserializationTest921.java b/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderWithTypeParametersTest.java similarity index 70% rename from src/test/java/com/fasterxml/jackson/failing/BuilderDeserializationTest921.java rename to src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderWithTypeParametersTest.java index 79beaee6f5..d8dcfa334a 100644 --- a/src/test/java/com/fasterxml/jackson/failing/BuilderDeserializationTest921.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderWithTypeParametersTest.java @@ -1,13 +1,16 @@ -package com.fasterxml.jackson.failing; +package com.fasterxml.jackson.databind.deser.builder; -import java.util.List; - -import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.LinkedHashMap; +import java.util.List; -public class BuilderDeserializationTest921 +public class BuilderWithTypeParametersTest extends BaseMapTest { public static class MyPOJO { @@ -77,7 +80,19 @@ public MyGenericPOJOWithCreator build() { } } - public void testWithBuilder() throws Exception { + public void testWithBuilderInferringBindings() throws Exception { + final ObjectMapper mapper = new ObjectMapper(); + mapper.enable(MapperFeature.INFER_BUILDER_TYPE_BINDINGS); + final String json = aposToQuotes("{ 'data': [ { 'x': 'x', 'y': 'y' } ] }"); + final MyGenericPOJO deserialized = + mapper.readValue(json, new TypeReference>() {}); + assertEquals(1, deserialized.data.size()); + Object ob = deserialized.data.get(0); + assertNotNull(ob); + assertEquals(MyPOJO.class, ob.getClass()); + } + + public void testWithBuilderWithoutInferringBindings() throws Exception { final ObjectMapper mapper = new ObjectMapper(); final String json = aposToQuotes("{ 'data': [ { 'x': 'x', 'y': 'y' } ] }"); final MyGenericPOJO deserialized = @@ -85,7 +100,7 @@ public void testWithBuilder() throws Exception { assertEquals(1, deserialized.data.size()); Object ob = deserialized.data.get(0); assertNotNull(ob); - assertEquals(MyPOJO.class, ob.getClass()); + assertEquals(LinkedHashMap.class, ob.getClass()); } public void testWithCreator() throws Exception { diff --git a/src/test/java/com/fasterxml/jackson/databind/type/TestTypeFactory.java b/src/test/java/com/fasterxml/jackson/databind/type/TestTypeFactory.java index f88355d900..6c0ca51f62 100644 --- a/src/test/java/com/fasterxml/jackson/databind/type/TestTypeFactory.java +++ b/src/test/java/com/fasterxml/jackson/databind/type/TestTypeFactory.java @@ -175,13 +175,27 @@ public void testParametricTypes() assertEquals(t, t2.containedType(1)); assertNull(t2.containedType(2)); - // and then custom generic type as well + // Then using TypeBindings + JavaType t3 = tf.constructParametricType(HashSet.class, t.getBindings()); // HashSet + assertEquals(CollectionType.class, t3.getClass()); + assertEquals(1, t3.containedTypeCount()); + assertEquals(strC, t3.containedType(0)); + assertNull(t3.containedType(1)); + + // Then custom generic type as well JavaType custom = tf.constructParametricType(SingleArgGeneric.class, String.class); assertEquals(SimpleType.class, custom.getClass()); assertEquals(1, custom.containedTypeCount()); assertEquals(strC, custom.containedType(0)); assertNull(custom.containedType(1)); + // and then custom generic type from TypeBindings + JavaType custom2 = tf.constructParametricType(SingleArgGeneric.class, t.getBindings()); + assertEquals(SimpleType.class, custom2.getClass()); + assertEquals(1, custom2.containedTypeCount()); + assertEquals(strC, custom2.containedType(0)); + assertNull(custom2.containedType(1)); + // And finally, ensure that we can't create invalid combinations try { // Maps must take 2 type parameters, not just one