Skip to content

Commit ef75bbd

Browse files
committed
Parameterized generic validation
1 parent 320e244 commit ef75bbd

File tree

2 files changed

+162
-12
lines changed

2 files changed

+162
-12
lines changed

src/main/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolver.java

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,59 @@ private static TypeVariable<?> maybeGetTypeVariable(Type type) {
132132
return null;
133133
}
134134

135-
private static boolean pessimisticallyValidateBounds(TypeResolutionContext context, JavaType boundType, Type[] upperBound) {
135+
/* Returns the TypeVariable if it can be extracted, otherwise null. */
136+
private static ParameterizedType maybeGetParameterizedType(Type type) {
137+
if (type instanceof ParameterizedType) {
138+
return (ParameterizedType) type;
139+
}
140+
// Extract simple type variables from wildcards matching '? extends T'
141+
if (type instanceof WildcardType) {
142+
WildcardType wildcardType = (WildcardType) type;
143+
// Exclude any form of '? super T'
144+
if (wildcardType.getLowerBounds().length != 0) {
145+
return null;
146+
}
147+
Type[] upperBounds = wildcardType.getUpperBounds();
148+
if (upperBounds.length == 1) {
149+
return maybeGetParameterizedType(upperBounds[0]);
150+
}
151+
}
152+
return null;
153+
}
154+
155+
private static boolean pessimisticallyValidateBounds(
156+
TypeResolutionContext context, JavaType boundType, Type[] upperBound) {
136157
for (Type type : upperBound) {
137-
// TODO: Additional validation would be helpful here, List<Object> is currently allowed given List<String> bounds.
138-
// Ideally we would narrow the requested bound type based on the _primaryType taking the upperBounds of the
139-
// method into account.
140-
if (!boundType.isTypeOrSubTypeOf(context.resolveType(type).getRawClass())) {
158+
if (!pessimisticallyValidateBound(context, boundType, type)) {
141159
return false;
142160
}
143161
}
144162
return true;
145163
}
146164

165+
private static boolean pessimisticallyValidateBound(
166+
TypeResolutionContext context, JavaType boundType, Type type) {
167+
if (!boundType.isTypeOrSubTypeOf(context.resolveType(type).getRawClass())) {
168+
return false;
169+
}
170+
ParameterizedType parameterized = maybeGetParameterizedType(type);
171+
if (parameterized != null) {
172+
Type[] typeArguments = parameterized.getActualTypeArguments();
173+
TypeBindings bindings = boundType.getBindings();
174+
if (bindings.size() != typeArguments.length) {
175+
return false;
176+
}
177+
for (int i = 0; i < bindings.size(); i++) {
178+
JavaType boundTypeBound = bindings.getBoundType(i);
179+
Type typeArg = typeArguments[i];
180+
if (!pessimisticallyValidateBound(context, boundTypeBound, typeArg)) {
181+
return false;
182+
}
183+
}
184+
}
185+
return true;
186+
}
187+
147188
private static boolean containsName(TypeVariable<?>[] typeVariables, String name) {
148189
if (typeVariables == null || name == null) {
149190
return false;

src/test/java/com/fasterxml/jackson/databind/ser/GenericTypeSerializationTest.java

Lines changed: 116 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,12 @@ public DD second() {
236236
}
237237
}
238238

239-
public static final class GenericSpecificityWrapperImpl<E, EE> {
239+
public static final class GenericSpecificityWrapper0<E, EE> {
240240

241241
private final E first;
242242
private final EE second;
243243

244-
GenericSpecificityWrapperImpl(E first, EE second) {
244+
GenericSpecificityWrapper0(E first, EE second) {
245245
this.first = first;
246246
this.second = second;
247247
}
@@ -255,8 +255,94 @@ public EE second() {
255255
}
256256

257257
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
258-
public static <F> GenericSpecificityWrapperImpl<?, F> fromJson(JsonGenericWrapper<Long, F> val) {
259-
return new GenericSpecificityWrapperImpl<>(val.first(), val.second());
258+
public static <F> GenericSpecificityWrapper0<?, F> fromJson(JsonGenericWrapper<Long, F> val) {
259+
return new GenericSpecificityWrapper0<>(val.first(), val.second());
260+
}
261+
}
262+
263+
public static final class GenericSpecificityWrapper1<E, EE> {
264+
265+
private final E first;
266+
private final EE second;
267+
268+
GenericSpecificityWrapper1(E first, EE second) {
269+
this.first = first;
270+
this.second = second;
271+
}
272+
273+
public E first() {
274+
return first;
275+
}
276+
277+
public EE second() {
278+
return second;
279+
}
280+
281+
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
282+
public static <F extends StringStubSubclass, FF> GenericSpecificityWrapper1<F, FF> fromJson(JsonGenericWrapper<F, FF> val) {
283+
return new GenericSpecificityWrapper1<>(val.first(), val.second());
284+
}
285+
}
286+
287+
public static class StringStub {
288+
private final String value;
289+
290+
private StringStub(String value) {
291+
this.value = value;
292+
}
293+
294+
@JsonCreator
295+
public static StringStub valueOf(String value) {
296+
return new StringStub(value);
297+
}
298+
}
299+
300+
public static class StringStubSubclass extends StringStub {
301+
302+
private StringStubSubclass(String value) {
303+
super(value);
304+
}
305+
306+
@JsonCreator
307+
public static StringStubSubclass valueOf(String value) {
308+
return new StringStubSubclass(value);
309+
}
310+
}
311+
312+
public static final class GenericSpecificityWrapper2<E, EE> {
313+
314+
private final E first;
315+
private final EE second;
316+
317+
GenericSpecificityWrapper2(E first, EE second) {
318+
this.first = first;
319+
this.second = second;
320+
}
321+
322+
public E first() {
323+
return first;
324+
}
325+
326+
public EE second() {
327+
return second;
328+
}
329+
330+
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
331+
public static <F extends Stub<StringStubSubclass>, FF> GenericSpecificityWrapper2<F, FF> fromJson(JsonGenericWrapper<F, FF> val) {
332+
return new GenericSpecificityWrapper2<>(val.first(), val.second());
333+
}
334+
}
335+
336+
public static class Stub<T> {
337+
private final T value;
338+
339+
private Stub(T value) {
340+
this.value = value;
341+
}
342+
343+
@JsonCreator
344+
public static <T> Stub<T> valueOf(T value) {
345+
return new Stub<>(value);
260346
}
261347
}
262348

@@ -391,17 +477,40 @@ public void testStaticDelegateDeserialization() throws Exception
391477
assertEquals("str", second);
392478
}
393479

394-
public void testStaticDelegateDeserialization_factoryProvidesSpecificity() throws Exception
480+
public void testStaticDelegateDeserialization_factoryProvidesSpecificity0() throws Exception
395481
{
396-
GenericSpecificityWrapperImpl<Object, Account> wrapper = MAPPER.readValue(
482+
GenericSpecificityWrapper0<Object, Account> wrapper = MAPPER.readValue(
397483
"{\"first\":\"1\",\"second\":{\"id\":1,\"name\":\"name\"}}",
398-
new TypeReference<GenericSpecificityWrapperImpl<Object, Account>>() {});
484+
new TypeReference<GenericSpecificityWrapper0<Object, Account>>() {});
399485
Object first = wrapper.first();
400486
assertEquals(Long.valueOf(1L), first);
401487
Account second = wrapper.second();
402488
assertEquals(new Account("name", 1L), second);
403489
}
404490

491+
public void testStaticDelegateDeserialization_factoryProvidesSpecificity1() throws Exception
492+
{
493+
GenericSpecificityWrapper1<StringStub, Account> wrapper = MAPPER.readValue(
494+
"{\"first\":\"1\",\"second\":{\"id\":1,\"name\":\"name\"}}",
495+
new TypeReference<GenericSpecificityWrapper1<StringStub, Account>>() {});
496+
StringStub first = wrapper.first();
497+
assertEquals("1", first.value);
498+
Account second = wrapper.second();
499+
assertEquals(new Account("name", 1L), second);
500+
}
501+
502+
public void testStaticDelegateDeserialization_factoryProvidesSpecificity2() throws Exception
503+
{
504+
GenericSpecificityWrapper2<Stub<Object>, Account> wrapper = MAPPER.readValue(
505+
"{\"first\":\"1\",\"second\":{\"id\":1,\"name\":\"name\"}}",
506+
new TypeReference<GenericSpecificityWrapper2<Stub<Object>, Account>>() {});
507+
Stub<Object> first = wrapper.first();
508+
StringStub stringStub = (StringStub) first.value;
509+
assertEquals("1", stringStub.value);
510+
Account second = wrapper.second();
511+
assertEquals(new Account("name", 1L), second);
512+
}
513+
405514
public void testStaticDelegateDeserialization_wildcardInResult() throws Exception
406515
{
407516
WildcardWrapperImpl<Account, Account> wrapper = MAPPER.readValue(

0 commit comments

Comments
 (0)