Skip to content

Commit 12161af

Browse files
Ville KoskelaVille Koskela
Ville Koskela
authored and
Ville Koskela
committed
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.
1 parent 159e268 commit 12161af

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

+12
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,18 @@ public enum MapperFeature implements ConfigFeature
198198
*/
199199
USE_STATIC_TYPING(false),
200200

201+
/**
202+
* Feature that enables inferring builder type bindings from the value type
203+
* being deserialized. This requires that the generic type declaration on
204+
* the value type match that on the builder exactly.
205+
*<p>
206+
* Feature is disabled by default which means that deserialization does
207+
* not support deserializing types via builders with type parameters.
208+
*<p>
209+
* See: https://github.com/FasterXML/jackson-databind/issues/921
210+
*/
211+
INFER_BUILDER_TYPE_BINDINGS(false),
212+
201213
/*
202214
/******************************************************
203215
/* View-related features

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

+10-4
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

+23-1
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,29 @@ public JavaType constructParametricType(Class<?> parametrized, Class<?>... param
894894
*/
895895
public JavaType constructParametricType(Class<?> rawType, JavaType... parameterTypes)
896896
{
897-
return _fromClass(null, rawType, TypeBindings.create(rawType, parameterTypes));
897+
return constructParametricType(rawType, TypeBindings.create(rawType, parameterTypes));
898+
}
899+
900+
/**
901+
* Factory method for constructing {@link JavaType} that
902+
* represents a parameterized type. The type's parameters are
903+
* specified as an instance of {@link TypeBindings}. This
904+
* is useful if you already have the type's parameters such
905+
* as those found on {@link JavaType}. For example, you could
906+
* call
907+
* <pre>
908+
* return TypeFactory.constructParametricType(ArrayList.class, javaType.getBindings());
909+
* </pre>
910+
* This effectively applies the parameterized types from one
911+
* {@link JavaType} to another class.
912+
*
913+
* @param rawType Actual type-erased type
914+
* @param parameterTypes Type bindings for the raw type
915+
* @since 3.0
916+
*/
917+
public JavaType constructParametricType(Class<?> rawType, TypeBindings parameterTypes)
918+
{
919+
return _fromClass(null, rawType, parameterTypes);
898920
}
899921

900922
/*

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

+23-8
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

+15-1
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)