From a32b34b135755408c8671a6dc95944d1eb966e44 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Mon, 19 Oct 2020 11:26:19 -0400 Subject: [PATCH] Implement JsonCreator factory generic type variable handling Previously TypeVariable handling worked in most cases due to coincidence, in most cases the type variable names used in factory methods and types use the same name, and order. However the way this worked resulted in bugs in edge cases, and was relatively fragile because it wasn't intended in the first place. Now we attempt to map type variables from the requested type through the factory function regardless of matching type variable names. --- .../introspect/AnnotatedCreatorCollector.java | 13 +- .../introspect/MethodGenericTypeResolver.java | 221 +++++++++++++ .../jackson/databind/type/TypeBindings.java | 13 +- .../MethodGenericTypeResolverTest.java | 183 +++++++++++ .../ser/GenericTypeSerializationTest.java | 299 +++++++++++++++++- 5 files changed, 723 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolver.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolverTest.java 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); + } }