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