diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedCreatorCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedCreatorCollector.java index d949168a29..798e36896d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedCreatorCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedCreatorCollector.java @@ -219,7 +219,10 @@ private List _findPotentialFactories(TypeFactory typeFactory, // 27-Oct-2020, tatu: SIGH. As per [databind#2894] there is widespread use of // incorrect bindings in the wild -- not supported (no tests) but used // nonetheless. So, for 2.11.x, put back "Bad Bindings"... - final TypeResolutionContext typeResCtxt = _typeContext; +// final TypeResolutionContext typeResCtxt = _typeContext; + + // 03-Nov-2020, ckozak: Implement generic JsonCreator TypeVariable handling [databind#2895] + final TypeResolutionContext emptyTypeResCtxt = new TypeResolutionContext.Empty(typeFactory); int factoryCount = candidates.size(); List result = new ArrayList<>(factoryCount); @@ -244,7 +247,7 @@ private List _findPotentialFactories(TypeFactory typeFactory, if (key.equals(methodKeys[i])) { result.set(i, constructFactoryCreator(candidates.get(i), - typeResCtxt, mixinFactory)); + emptyTypeResCtxt, mixinFactory)); break; } } @@ -254,8 +257,12 @@ private List _findPotentialFactories(TypeFactory typeFactory, for (int i = 0; i < factoryCount; ++i) { AnnotatedMethod factory = result.get(i); if (factory == null) { + Method candidate = candidates.get(i); + // Apply generic type information based on the requested type + TypeResolutionContext typeResCtxt = MethodGenericTypeResolver.narrowMethodTypeParameters( + candidate, type, typeFactory, emptyTypeResCtxt); result.set(i, - constructFactoryCreator(candidates.get(i), + constructFactoryCreator(candidate, typeResCtxt, null)); } } diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolver.java b/src/main/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolver.java new file mode 100644 index 0000000000..b40513b681 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolver.java @@ -0,0 +1,221 @@ +package com.fasterxml.jackson.databind.introspect; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeBindings; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Objects; + +/** + * Internal utility functionality to handle type resolution for method type variables based on the requested + * result type. + */ +final class MethodGenericTypeResolver { + + /* + * Attempt to narrow types on a generic factory method based on the expected result (requestedType). + * If narrowing was possible, a new TypeResolutionContext is returned with the discovered TypeBindings, + * otherwise the emptyTypeResCtxt argument is returned. + * + * For example: + * Given type Wrapper with + * @JsonCreator static Wrapper fromJson(T value) + * When a Wrapper is requested the factory must return a Wrapper and we can bind T to Duck + * as though the method was written with defined types: + * @JsonCreator static Wrapper fromJson(Duck value) + */ + static TypeResolutionContext narrowMethodTypeParameters( + Method candidate, + JavaType requestedType, + TypeFactory typeFactory, + TypeResolutionContext emptyTypeResCtxt) { + TypeBindings newTypeBindings = bindMethodTypeParameters(candidate, requestedType, emptyTypeResCtxt); + return newTypeBindings == null + ? emptyTypeResCtxt + : new TypeResolutionContext.Basic(typeFactory, newTypeBindings); + } + + /** + * Returns {@link TypeBindings} with additional type information + * based on {@code requestedType} if possible, otherwise {@code null}. + */ + static TypeBindings bindMethodTypeParameters( + Method candidate, + JavaType requestedType, + TypeResolutionContext emptyTypeResCtxt) { + TypeVariable[] methodTypeParameters = candidate.getTypeParameters(); + if (methodTypeParameters.length == 0 + // If the primary type has no type parameters, there's nothing to do + || requestedType.getBindings().isEmpty()) { + // Method has no type parameters: no need to modify the resolution context. + return null; + } + Type genericReturnType = candidate.getGenericReturnType(); + if (!(genericReturnType instanceof ParameterizedType)) { + // Return value is not parameterized, it cannot be used to associate the requestedType expectations + // onto parameters. + return null; + } + + ParameterizedType parameterizedGenericReturnType = (ParameterizedType) genericReturnType; + // Primary type and result type must be the same class, otherwise we would need to + // trace generic parameters to a common superclass or interface. + if (!Objects.equals(requestedType.getRawClass(), parameterizedGenericReturnType.getRawType())) { + return null; + } + + // Construct TypeBindings based on the requested type, and type variables that occur in the generic return type. + // For example given requestedType: Foo + // and method static Foo func(Bar in) + // Produces TypeBindings{T=String, U=Int}. + Type[] methodReturnTypeArguments = parameterizedGenericReturnType.getActualTypeArguments(); + ArrayList names = new ArrayList<>(methodTypeParameters.length); + ArrayList types = new ArrayList<>(methodTypeParameters.length); + for (int i = 0; i < methodReturnTypeArguments.length; i++) { + Type methodReturnTypeArgument = methodReturnTypeArguments[i]; + // Note: This strictly supports only TypeVariables of the forms "T" and "? extends T", + // not complex wildcards with nested type variables + TypeVariable typeVar = maybeGetTypeVariable(methodReturnTypeArgument); + if (typeVar != null) { + String typeParameterName = typeVar.getName(); + if (typeParameterName == null) { + return null; + } + + JavaType bindTarget = requestedType.getBindings().getBoundType(i); + if (bindTarget == null) { + return null; + } + // If the type parameter name is not present in the method type parameters we + // fall back to default type handling. + TypeVariable methodTypeVariable = findByName(methodTypeParameters, typeParameterName); + if (methodTypeVariable == null) { + return null; + } + if (pessimisticallyValidateBounds(emptyTypeResCtxt, bindTarget, methodTypeVariable.getBounds())) { + // Avoid duplicate entries for the same type variable, e.g. ' Map foo(Class in)' + int existingIndex = names.indexOf(typeParameterName); + if (existingIndex != -1) { + JavaType existingBindTarget = types.get(existingIndex); + if (bindTarget.equals(existingBindTarget)) { + continue; + } + boolean existingIsSubtype = existingBindTarget.isTypeOrSubTypeOf(bindTarget.getRawClass()); + boolean newIsSubtype = bindTarget.isTypeOrSubTypeOf(existingBindTarget.getRawClass()); + if (!existingIsSubtype && !newIsSubtype) { + // No way to satisfy the requested type. + return null; + } + if (existingIsSubtype ^ newIsSubtype && newIsSubtype) { + // If the new type is more specific than the existing type, the new type replaces the old. + types.set(existingIndex, bindTarget); + } + } else { + names.add(typeParameterName); + types.add(bindTarget); + } + } + } + } + // Fall back to default handling if no specific types from the requestedType are used + if (names.isEmpty()) { + return null; + } + return TypeBindings.create(names, types); + } + + /* Returns the TypeVariable if it can be extracted, otherwise null. */ + private static TypeVariable maybeGetTypeVariable(Type type) { + if (type instanceof TypeVariable) { + return (TypeVariable) type; + } + // Extract simple type variables from wildcards matching '? extends T' + if (type instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) type; + // Exclude any form of '? super T' + if (wildcardType.getLowerBounds().length != 0) { + return null; + } + Type[] upperBounds = wildcardType.getUpperBounds(); + if (upperBounds.length == 1) { + return maybeGetTypeVariable(upperBounds[0]); + } + } + return null; + } + + /* Returns the TypeVariable if it can be extracted, otherwise null. */ + private static ParameterizedType maybeGetParameterizedType(Type type) { + if (type instanceof ParameterizedType) { + return (ParameterizedType) type; + } + // Extract simple type variables from wildcards matching '? extends T' + if (type instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) type; + // Exclude any form of '? super T' + if (wildcardType.getLowerBounds().length != 0) { + return null; + } + Type[] upperBounds = wildcardType.getUpperBounds(); + if (upperBounds.length == 1) { + return maybeGetParameterizedType(upperBounds[0]); + } + } + return null; + } + + private static boolean pessimisticallyValidateBounds( + TypeResolutionContext context, JavaType boundType, Type[] upperBound) { + for (Type type : upperBound) { + if (!pessimisticallyValidateBound(context, boundType, type)) { + return false; + } + } + return true; + } + + private static boolean pessimisticallyValidateBound( + TypeResolutionContext context, JavaType boundType, Type type) { + if (!boundType.isTypeOrSubTypeOf(context.resolveType(type).getRawClass())) { + return false; + } + ParameterizedType parameterized = maybeGetParameterizedType(type); + if (parameterized != null) { + Type[] typeArguments = parameterized.getActualTypeArguments(); + TypeBindings bindings = boundType.getBindings(); + if (bindings.size() != typeArguments.length) { + return false; + } + for (int i = 0; i < bindings.size(); i++) { + JavaType boundTypeBound = bindings.getBoundType(i); + Type typeArg = typeArguments[i]; + if (!pessimisticallyValidateBound(context, boundTypeBound, typeArg)) { + return false; + } + } + } + return true; + } + + private static TypeVariable findByName(TypeVariable[] typeVariables, String name) { + if (typeVariables == null || name == null) { + return null; + } + for (TypeVariable typeVariable : typeVariables) { + if (name.equals(typeVariable.getName())) { + return typeVariable; + } + } + return null; + } + + private MethodGenericTypeResolver() { + // Utility class + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/type/TypeBindings.java b/src/main/java/com/fasterxml/jackson/databind/type/TypeBindings.java index 3dcea8a0e9..b9422b2bc7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/type/TypeBindings.java +++ b/src/main/java/com/fasterxml/jackson/databind/type/TypeBindings.java @@ -142,7 +142,18 @@ public static TypeBindings create(Class erasedType, JavaType typeArg1, JavaTy return new TypeBindings(new String[] { vars[0].getName(), vars[1].getName() }, new JavaType[] { typeArg1, typeArg2 }, null); } - + + /** + * Factory method for constructing bindings given names and associated types. + */ + public static TypeBindings create(List names, List types) + { + if (names == null || names.isEmpty() || types == null || types.isEmpty()) { + return EMPTY; + } + return new TypeBindings(names.toArray(NO_STRINGS), types.toArray(NO_TYPES), null); + } + /** * Alternate factory method that may be called if it is possible that type * does or does not require type parameters; this is mostly useful for diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolverTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolverTest.java new file mode 100644 index 0000000000..c3c2be4554 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolverTest.java @@ -0,0 +1,183 @@ +package com.fasterxml.jackson.databind.introspect; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeBindings; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +public class MethodGenericTypeResolverTest extends BaseMapTest { + + private static final TypeResolutionContext EMPTY_CONTEXT = + new TypeResolutionContext.Empty(TypeFactory.defaultInstance()); + + public static AtomicReference simple(T input) { + throw new UnsupportedOperationException(); + } + + public static AtomicReference noGenerics(String input) { + throw new UnsupportedOperationException(); + } + + public static Map mapWithSameKeysAndValues(List input) { + throw new UnsupportedOperationException(); + } + + public static Map disconnected(List input) { + throw new UnsupportedOperationException(); + } + + public static Map multipleTypeVariables(Map input) { + throw new UnsupportedOperationException(); + } + + public static Map multipleTypeVariablesWithUpperBound(Map input) { + throw new UnsupportedOperationException(); + } + + public static class StubA { + private final String value; + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + private StubA(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + public static class StubB extends StubA { + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public StubB(String value) { + super(value); + } + } + + public void testWithoutGenerics() { + TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters( + method("noGenerics"), type(String.class), EMPTY_CONTEXT); + assertNull(bindings); + } + + public void testWithoutGenericsInResult() { + TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters( + method("simple"), type(AtomicReference.class), EMPTY_CONTEXT); + assertNull(bindings); + } + + public void testResultDoesNotUseTypeVariables() { + TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters( + method("disconnected"), type(new TypeReference>() { + }), EMPTY_CONTEXT); + assertNull(bindings); + } + + public void testWithoutGenericsInMethod() { + TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters( + method("noGenerics"), type(new TypeReference>() { + }), EMPTY_CONTEXT); + assertNull(bindings); + } + + public void testWithRepeatedGenericInReturn() { + TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters( + method("mapWithSameKeysAndValues"), type(new TypeReference>() { + }), EMPTY_CONTEXT); + assertEquals(asMap("T", type(String.class)), asMap(bindings)); + } + + public void testWithRepeatedGenericInReturnWithIncreasingSpecificity() { + Method method = method("mapWithSameKeysAndValues"); + TypeBindings bindingsAb = MethodGenericTypeResolver.bindMethodTypeParameters( + method, type(new TypeReference>() { + }), EMPTY_CONTEXT); + TypeBindings bindingsBa = MethodGenericTypeResolver.bindMethodTypeParameters( + method, type(new TypeReference>() { + }), EMPTY_CONTEXT); + assertEquals(asMap(bindingsBa), asMap(bindingsAb)); + assertEquals(asMap(bindingsBa), asMap("T", type(StubB.class))); + } + + public void testMultipleTypeVariables() { + TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters( + method("multipleTypeVariables"), type(new TypeReference>() { + }), EMPTY_CONTEXT); + assertEquals( + asMap("A", type(Integer.class), "B", type(Long.class)), + asMap(bindings)); + } + + public void testMultipleTypeVariablesWithUpperBounds() { + TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters( + method("multipleTypeVariablesWithUpperBound"), type(new TypeReference>() { + }), EMPTY_CONTEXT); + assertEquals( + asMap("A", type(Integer.class), "B", type(Long.class)), + asMap(bindings)); + } + + public void testResultTypeDoesNotExactlyMatch() { + TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters( + method("multipleTypeVariables"), type(new TypeReference>() { + }), EMPTY_CONTEXT); + // Mapping the result to a common supertype is not supported. + assertNull(bindings); + } + + private static Method method(String name) { + Method result = null; + for (Method method : MethodGenericTypeResolverTest.class.getMethods()) { + if (Modifier.isStatic(method.getModifiers()) && name.equals(method.getName())) { + if (result != null) { + throw new AssertionError("Multiple methods discovered with name " + + name + ": " + result + " and " + method); + } + result = method; + } + } + assertNotNull("Failed to find method", result); + return result; + } + + private static JavaType type(TypeReference reference) { + return type(reference.getType()); + } + + private static JavaType type(Type type) { + return EMPTY_CONTEXT.resolveType(type); + } + + private static Map asMap(TypeBindings bindings) { + assertNotNull(bindings); + Map result = new HashMap<>(bindings.size()); + for (int i = 0; i < bindings.size(); i++) { + result.put(bindings.getBoundName(i), bindings.getBoundType(i)); + } + assertEquals(bindings.size(), result.size()); + return result; + } + + private static Map asMap(String name, JavaType javaType) { + return Collections.singletonMap(name, javaType); + } + + private static Map asMap( + String name0, JavaType javaType0, String name1, JavaType javaType1) { + Map result = new HashMap<>(2); + result.put(name0, javaType0); + result.put(name1, javaType1); + return result; + } +} \ No newline at end of file diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/GenericTypeSerializationTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/GenericTypeSerializationTest.java index 67440104e3..dbb8f6e90f 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/GenericTypeSerializationTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/GenericTypeSerializationTest.java @@ -8,20 +8,38 @@ import com.fasterxml.jackson.databind.BaseMapTest; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; public class GenericTypeSerializationTest extends BaseMapTest { static class Account { private Long id; private String name; - - public Account(String name, Long id) { + + @JsonCreator + public Account( + @JsonProperty("name") String name, + @JsonProperty("id") Long id) { this.id = id; this.name = name; } public String getName() { return name; } public Long getId() { return id; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Account account = (Account) o; + return Objects.equals(id, account.id) && Objects.equals(name, account.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } } static class Key { @@ -164,6 +182,219 @@ public static Attributes2821 dummyMethod(Map attributes) { } } + @JsonSerialize(as = GenericWrapperImpl.class) + @JsonDeserialize(as = GenericWrapperImpl.class) + public interface GenericWrapper { + A first(); + AA second(); + } + + public static final class GenericWrapperImpl implements GenericWrapper { + + private final B first; + private final BB second; + + GenericWrapperImpl(B first, BB second) { + this.first = first; + this.second = second; + } + + @Override + public B first() { + return first; + } + + @Override + public BB second() { + return second; + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + // Invert the type parameter order to make things exciting! + public static GenericWrapperImpl fromJson(JsonGenericWrapper val) { + return new GenericWrapperImpl<>(val.first(), val.second()); + } + } + + @JsonDeserialize + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE) + public static final class JsonGenericWrapper implements GenericWrapper { + + @JsonProperty("first") + private D first; + + @JsonProperty("second") + private DD second; + + @Override + @JsonProperty("first") + public D first() { + return first; + } + + @Override + @JsonProperty("second") + public DD second() { + return second; + } + } + + public static final class GenericSpecificityWrapper0 { + + private final E first; + private final EE second; + + GenericSpecificityWrapper0(E first, EE second) { + this.first = first; + this.second = second; + } + + public E first() { + return first; + } + + public EE second() { + return second; + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static GenericSpecificityWrapper0 fromJson(JsonGenericWrapper val) { + return new GenericSpecificityWrapper0<>(val.first(), val.second()); + } + } + + public static final class GenericSpecificityWrapper1 { + + private final E first; + private final EE second; + + GenericSpecificityWrapper1(E first, EE second) { + this.first = first; + this.second = second; + } + + public E first() { + return first; + } + + public EE second() { + return second; + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static GenericSpecificityWrapper1 fromJson(JsonGenericWrapper val) { + return new GenericSpecificityWrapper1<>(val.first(), val.second()); + } + } + + public static class StringStub { + private final String value; + + private StringStub(String value) { + this.value = value; + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static StringStub valueOf(String value) { + return new StringStub(value); + } + } + + public static class StringStubSubclass extends StringStub { + + private StringStubSubclass(String value) { + super(value); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static StringStubSubclass valueOf(String value) { + return new StringStubSubclass(value); + } + } + + public static final class GenericSpecificityWrapper2 { + + private final E first; + private final EE second; + + GenericSpecificityWrapper2(E first, EE second) { + this.first = first; + this.second = second; + } + + public E first() { + return first; + } + + public EE second() { + return second; + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static , FF> GenericSpecificityWrapper2 fromJson(JsonGenericWrapper val) { + return new GenericSpecificityWrapper2<>(val.first(), val.second()); + } + } + + public static class Stub { + private final T value; + + private Stub(T value) { + this.value = value; + } + + @JsonCreator + public static Stub valueOf(T value) { + return new Stub<>(value); + } + } + + public static final class WildcardWrapperImpl { + + private final G first; + private final GG second; + + WildcardWrapperImpl(G first, GG second) { + this.first = first; + this.second = second; + } + + public G first() { + return first; + } + + public GG second() { + return second; + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static WildcardWrapperImpl fromJson(JsonGenericWrapper val) { + return new WildcardWrapperImpl<>(val.first(), val.second()); + } + } + + public static class SimpleWrapper { + + private final T value; + + SimpleWrapper(T value) { + this.value = value; + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static SimpleWrapper fromJson(JsonSimpleWrapper value) { + return new SimpleWrapper<>(value.object); + } + } + + @JsonDeserialize + public static final class JsonSimpleWrapper { + + @JsonProperty("object") + public T object; + + } + /* /********************************************************** /* Unit tests @@ -260,4 +491,68 @@ public void testTypeResolution2821() throws Exception String json = MAPPER.writeValueAsString(val); assertNotNull(json); } + + public void testStaticDelegateDeserialization() throws Exception + { + GenericWrapper wrapper = MAPPER.readValue( + "{\"first\":{\"id\":1,\"name\":\"name\"},\"second\":\"str\"}", + new TypeReference>() {}); + Account account = wrapper.first(); + assertEquals(new Account("name", 1L), account); + String second = wrapper.second(); + assertEquals("str", second); + } + + public void testStaticDelegateDeserialization_factoryProvidesSpecificity0() throws Exception + { + GenericSpecificityWrapper0 wrapper = MAPPER.readValue( + "{\"first\":\"1\",\"second\":{\"id\":1,\"name\":\"name\"}}", + new TypeReference>() {}); + Object first = wrapper.first(); + assertEquals(Long.valueOf(1L), first); + Account second = wrapper.second(); + assertEquals(new Account("name", 1L), second); + } + + public void testStaticDelegateDeserialization_factoryProvidesSpecificity1() throws Exception + { + GenericSpecificityWrapper1 wrapper = MAPPER.readValue( + "{\"first\":\"1\",\"second\":{\"id\":1,\"name\":\"name\"}}", + new TypeReference>() {}); + StringStub first = wrapper.first(); + assertEquals("1", first.value); + Account second = wrapper.second(); + assertEquals(new Account("name", 1L), second); + } + + public void testStaticDelegateDeserialization_factoryProvidesSpecificity2() throws Exception + { + GenericSpecificityWrapper2, Account> wrapper = MAPPER.readValue( + "{\"first\":\"1\",\"second\":{\"id\":1,\"name\":\"name\"}}", + new TypeReference, Account>>() {}); + Stub first = wrapper.first(); + StringStub stringStub = (StringStub) first.value; + assertEquals("1", stringStub.value); + Account second = wrapper.second(); + assertEquals(new Account("name", 1L), second); + } + + public void testStaticDelegateDeserialization_wildcardInResult() throws Exception + { + WildcardWrapperImpl wrapper = MAPPER.readValue( + "{\"first\":{\"id\":1,\"name\":\"name1\"},\"second\":{\"id\":2,\"name\":\"name2\"}}", + new TypeReference>() {}); + Account account1 = wrapper.first(); + assertEquals(new Account("name1", 1L), account1); + Account account2 = wrapper.second(); + assertEquals(new Account("name2", 2L), account2); + } + + public void testSimpleStaticJsonCreator() throws Exception + { + SimpleWrapper wrapper = MAPPER.readValue("{\"object\":{\"id\":1,\"name\":\"name1\"}}", + new TypeReference>() {}); + Account account = wrapper.value; + assertEquals(new Account("name1", 1L), account); + } }