Skip to content

Commit 3501f29

Browse files
vjkoskelacowtowncoder
authored andcommitted
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. (#1796)
1 parent 97ae341 commit 3501f29

File tree

5 files changed

+83
-14
lines changed

5 files changed

+83
-14
lines changed

src/main/java/com/fasterxml/jackson/databind/MapperFeature.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,18 @@ public enum MapperFeature implements ConfigFeature
188188
*/
189189
USE_STATIC_TYPING(false),
190190

191+
/**
192+
* Feature that enables inferring builder type bindings from the value type
193+
* being deserialized. This requires that the generic type declaration on
194+
* the value type match that on the builder exactly.
195+
*<p>
196+
* Feature is disabled by default which means that deserialization does
197+
* not support deserializing types via builders with type parameters.
198+
*<p>
199+
* See: https://github.com/FasterXML/jackson-databind/issues/921
200+
*/
201+
INFER_BUILDER_TYPE_BINDINGS(false),
202+
191203
/*
192204
/******************************************************
193205
/* View-related features

src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,18 @@ public JsonDeserializer<Object> createBeanDeserializer(DeserializationContext ct
138138
}
139139

140140
@Override
141-
public JsonDeserializer<Object> createBuilderBasedDeserializer(DeserializationContext ctxt,
142-
JavaType valueType, BeanDescription beanDesc, Class<?> builderClass)
143-
throws JsonMappingException
141+
public JsonDeserializer<Object> createBuilderBasedDeserializer(
142+
DeserializationContext ctxt, JavaType valueType, BeanDescription beanDesc,
143+
Class<?> builderClass)
144+
throws JsonMappingException
144145
{
145146
// First: need a BeanDescription for builder class
146-
JavaType builderType = ctxt.constructType(builderClass);
147+
JavaType builderType;
148+
if (ctxt.isEnabled(MapperFeature.INFER_BUILDER_TYPE_BINDINGS)) {
149+
builderType = ctxt.getTypeFactory().constructParametricType(builderClass, valueType.getBindings());
150+
} else {
151+
builderType = ctxt.constructType(builderClass);
152+
}
147153
BeanDescription builderDesc = ctxt.getConfig().introspectForBuilder(builderType);
148154
return buildBuilderBasedDeserializer(ctxt, valueType, builderDesc);
149155
}

src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,29 @@ public JavaType constructParametricType(Class<?> parametrized, Class<?>... param
874874
*/
875875
public JavaType constructParametricType(Class<?> rawType, JavaType... parameterTypes)
876876
{
877-
return _fromClass(null, rawType, TypeBindings.create(rawType, parameterTypes));
877+
return constructParametricType(rawType, TypeBindings.create(rawType, parameterTypes));
878+
}
879+
880+
/**
881+
* Factory method for constructing {@link JavaType} that
882+
* represents a parameterized type. The type's parameters are
883+
* specified as an instance of {@link TypeBindings}. This
884+
* is useful if you already have the type's parameters such
885+
* as those found on {@link JavaType}. For example, you could
886+
* call
887+
* <pre>
888+
* return TypeFactory.constructParametricType(ArrayList.class, javaType.getBindings());
889+
* </pre>
890+
* This effectively applies the parameterized types from one
891+
* {@link JavaType} to another class.
892+
*
893+
* @param rawType Actual type-erased type
894+
* @param parameterTypes Type bindings for the raw type
895+
* @since 3.0
896+
*/
897+
public JavaType constructParametricType(Class<?> rawType, TypeBindings parameterTypes)
898+
{
899+
return _fromClass(null, rawType, parameterTypes);
878900
}
879901

880902
/*

src/test/java/com/fasterxml/jackson/failing/BuilderDeserializationTest921.java renamed to src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderWithTypeParametersTest.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
package com.fasterxml.jackson.failing;
1+
package com.fasterxml.jackson.databind.deser.builder;
22

3-
import java.util.List;
4-
5-
import com.fasterxml.jackson.annotation.*;
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
65
import com.fasterxml.jackson.core.type.TypeReference;
7-
import com.fasterxml.jackson.databind.*;
6+
import com.fasterxml.jackson.databind.BaseMapTest;
7+
import com.fasterxml.jackson.databind.MapperFeature;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
89
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
10+
import java.util.LinkedHashMap;
11+
import java.util.List;
912

10-
public class BuilderDeserializationTest921
13+
public class BuilderWithTypeParametersTest
1114
extends BaseMapTest
1215
{
1316
public static class MyPOJO {
@@ -77,15 +80,27 @@ public MyGenericPOJOWithCreator<T> build() {
7780
}
7881
}
7982

80-
public void testWithBuilder() throws Exception {
83+
public void testWithBuilderInferringBindings() throws Exception {
84+
final ObjectMapper mapper = new ObjectMapper();
85+
mapper.enable(MapperFeature.INFER_BUILDER_TYPE_BINDINGS);
86+
final String json = aposToQuotes("{ 'data': [ { 'x': 'x', 'y': 'y' } ] }");
87+
final MyGenericPOJO<MyPOJO> deserialized =
88+
mapper.readValue(json, new TypeReference<MyGenericPOJO<MyPOJO>>() {});
89+
assertEquals(1, deserialized.data.size());
90+
Object ob = deserialized.data.get(0);
91+
assertNotNull(ob);
92+
assertEquals(MyPOJO.class, ob.getClass());
93+
}
94+
95+
public void testWithBuilderWithoutInferringBindings() throws Exception {
8196
final ObjectMapper mapper = new ObjectMapper();
8297
final String json = aposToQuotes("{ 'data': [ { 'x': 'x', 'y': 'y' } ] }");
8398
final MyGenericPOJO<MyPOJO> deserialized =
8499
mapper.readValue(json, new TypeReference<MyGenericPOJO<MyPOJO>>() {});
85100
assertEquals(1, deserialized.data.size());
86101
Object ob = deserialized.data.get(0);
87102
assertNotNull(ob);
88-
assertEquals(MyPOJO.class, ob.getClass());
103+
assertEquals(LinkedHashMap.class, ob.getClass());
89104
}
90105

91106
public void testWithCreator() throws Exception {

src/test/java/com/fasterxml/jackson/databind/type/TestTypeFactory.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,27 @@ public void testParametricTypes()
175175
assertEquals(t, t2.containedType(1));
176176
assertNull(t2.containedType(2));
177177

178-
// and then custom generic type as well
178+
// Then using TypeBindings
179+
JavaType t3 = tf.constructParametricType(HashSet.class, t.getBindings()); // HashSet<String>
180+
assertEquals(CollectionType.class, t3.getClass());
181+
assertEquals(1, t3.containedTypeCount());
182+
assertEquals(strC, t3.containedType(0));
183+
assertNull(t3.containedType(1));
184+
185+
// Then custom generic type as well
179186
JavaType custom = tf.constructParametricType(SingleArgGeneric.class, String.class);
180187
assertEquals(SimpleType.class, custom.getClass());
181188
assertEquals(1, custom.containedTypeCount());
182189
assertEquals(strC, custom.containedType(0));
183190
assertNull(custom.containedType(1));
184191

192+
// and then custom generic type from TypeBindings
193+
JavaType custom2 = tf.constructParametricType(SingleArgGeneric.class, t.getBindings());
194+
assertEquals(SimpleType.class, custom2.getClass());
195+
assertEquals(1, custom2.containedTypeCount());
196+
assertEquals(strC, custom2.containedType(0));
197+
assertNull(custom2.containedType(1));
198+
185199
// And finally, ensure that we can't create invalid combinations
186200
try {
187201
// Maps must take 2 type parameters, not just one

0 commit comments

Comments
 (0)