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 08455bf5d3..5787df8de0 100644 --- a/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java @@ -480,23 +480,82 @@ private TypeBindings _bindingsForSubtype(JavaType baseType, int typeParamCount, // (hopefully passing null Class for root is ok) int baseCount = baseType.containedTypeCount(); if (baseCount == typeParamCount) { - if (typeParamCount == 1) { - return TypeBindings.create(subclass, baseType.containedType(0)); - } - if (typeParamCount == 2) { - return TypeBindings.create(subclass, baseType.containedType(0), - baseType.containedType(1)); - } - List types = new ArrayList(baseCount); + // 2017-10-17 epollan [databind#1604] + // Given "stacks" of type bindings contained in `baseType`; e.g. {@code baseType : Foo, X>}, + // with bindings A[B] (indicating a type binding to the JavaType for `A` with a nested + // type binding to `B`) and X; pop off all type bindings that might be implied in the + // `subclass`'s definition relative to the `baseType`. + // E.g. {@code subclass : Bar} where {@code Bar extends Foo, U>}, would + // inherit type bindings B and X. It would _not_ inherit `baseType`'s type binding to A[B] + // -- it would only inherit the inner type binding to B. + // This is because {@code Bar} already _is a_ {@code Foo, ?>}, and serialization config + // for the class itself already comprehends the types that the inheritance specification + // has structurally locked into the base type's bindings. + JavaType[] bindings = new JavaType[baseCount]; for (int i = 0; i < baseCount; ++i) { - types.add(baseType.containedType(i)); + bindings[i] = _bindingForSubtypeAtBindingPosition(baseType, i, subclass); } - return TypeBindings.create(subclass, types); + return TypeBindings.create(subclass, bindings); } // Otherwise, two choices: match N first, or empty. Do latter, for now return TypeBindings.emptyBindings(); } + private JavaType _bindingForSubtypeAtBindingPosition(JavaType baseType, int bindingPosition, Class subclass) { + JavaType binding = baseType.containedType(bindingPosition); + // While the type binding is nested with exactly one inner binding, AND the subtype's definition + // itself already implies the outermost binding, then... + while (binding.containedTypeCount() == 1 && _subtypeImpliesBinding(baseType, binding, bindingPosition, subclass)) { + // ...peel the outer binding off + binding = binding.containedType(0); + } + return binding; + } + + /** + * True if the subclass already implies the base type's binding at {@code bindingPosition} + * (see {@link JavaType#containedType}). An example of an "implied" binding would be where + * the subclass is the erased class `C` (defined as {@code C extends A>}). In this scenario, `A` + * would be the baseType and would be bound to `B` with a _nested_ inner binding to some type + * for `T`. `C` structurally implies the `B` binding without it being present as a type parameter -- + * it's part of the class definition, itself. + */ + private boolean _subtypeImpliesBinding(JavaType baseType, JavaType baseTypeBinding, int bindingPosition, Class subclass) { + // Should take a A, D>, e.g., and make it A, D> when bindingPosition == 0, e.g. + JavaType wildcardedBase = _fromClass( + null, + baseType.getRawClass(), + _wildcardedBindings(baseType, baseTypeBinding, bindingPosition) + ); + + JavaType[] wildcards = new JavaType[subclass.getTypeParameters().length]; + Arrays.fill(wildcards, CORE_TYPE_OBJECT); + JavaType wildcardedSubclass = _fromClass( + null, + subclass, + TypeBindings.create( + subclass, + wildcards + ) + ); + return wildcardedSubclass.findSuperType(baseType.getRawClass()).equals(wildcardedBase); + } + + /** + * Should take a {@code A, D>}, e.g., and make it {@code A, D>} when {@code bindingPosition == 0}, e.g. + * Basically, reconstruct the base type's bindings by "wildcarding" the binding at the given position. All + * other bindings are used as-is. + */ + private TypeBindings _wildcardedBindings(JavaType baseType, JavaType baseTypeBinding, int bindingPosition) { + JavaType[] bindingTypes = new JavaType[baseType.containedTypeCount()]; + for (int i = 0; i < bindingTypes.length; i++) { + bindingTypes[i] = (i == bindingPosition) ? + _fromClass(null, baseTypeBinding.getRawClass(), TypeBindings.create(baseTypeBinding.getRawClass(), CORE_TYPE_OBJECT)) : + baseType.containedType(i); + } + return TypeBindings.create(baseType.getRawClass(), bindingTypes); + } + /** * Method similar to {@link #constructSpecializedType}, but that creates a * less-specific type of given type. Usually this is as simple as simply diff --git a/src/test/java/com/fasterxml/jackson/databind/type/NestedTypes1604Test.java b/src/test/java/com/fasterxml/jackson/databind/type/NestedTypes1604Test.java new file mode 100644 index 0000000000..155ead29f1 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/type/NestedTypes1604Test.java @@ -0,0 +1,84 @@ +package com.fasterxml.jackson.databind.type; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.databind.*; + +// for [databind#1604] +public class NestedTypes1604Test extends BaseMapTest { + public static class Data { + private T data; + + public Data(T data) { + this.data = data; + } + + public T getData() { + return data; + } + + public static Data> of(List data) { + return new DataList<>(data); + } + } + + public static class DataList extends Data> { + public DataList(List data) { + super(data); + } + } + + public static class Inner { + private int index; + + public Inner(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + } + + public static class BadOuter { + private Data> inner; + + public BadOuter(Data> inner) { + this.inner = inner; + } + + public Data> getInner() { + return inner; + } + } + + public static class GoodOuter { + private DataList inner; + + public GoodOuter(DataList inner) { + this.inner = inner; + } + + public DataList getInner() { + return inner; + } + } + + public void testIssue1604() throws Exception { + final ObjectMapper objectMapper = objectMapper(); + List inners = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + inners.add(new Inner(i)); + } + String expectedJson = aposToQuotes("{'inner':{'data':[{'index':0},{'index':1}]}}"); + assertEquals( + expectedJson, + objectMapper.writeValueAsString(new GoodOuter(new DataList<>(inners))) + ); + assertEquals( + expectedJson, + objectMapper.writeValueAsString(new BadOuter(Data.of(inners))) + ); + } +}