TypeResolutionTest.java
package tools.jackson.databind.type;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.util.*;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.*;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
@SuppressWarnings("serial")
public class TypeResolutionTest extends DatabindTestUtil
{
public static class LongValuedMap<K> extends HashMap<K, Long> { }
static class GenericList<X> extends ArrayList<X> { }
static class GenericList2<Y> extends GenericList<Y> { }
static class LongList extends GenericList2<Long> { }
static class MyLongList<T> extends LongList { }
static class Range<E extends Comparable<E>> implements Serializable
{
public Range(E start, E end) { }
}
static class DoubleRange extends Range<Double> {
public DoubleRange() { super(null, null); }
public DoubleRange(Double s, Double e) { super(s, e); }
}
/*
/**********************************************************
/* Test methods
/**********************************************************
*/
@Test
public void testMaps()
{
TypeFactory tf = defaultTypeFactory();
JavaType t = tf.constructType(new TypeReference<LongValuedMap<String>>() { });
MapType type = (MapType) t;
assertSame(LongValuedMap.class, type.getRawClass());
assertEquals(tf.constructType(String.class), type.getKeyType());
assertEquals(tf.constructType(Long.class), type.getContentType());
}
@Test
public void testListViaTypeRef()
{
TypeFactory tf = defaultTypeFactory();
JavaType t = tf.constructType(new TypeReference<MyLongList<Integer>>() {});
CollectionType type = (CollectionType) t;
assertSame(MyLongList.class, type.getRawClass());
assertEquals(tf.constructType(Long.class), type.getContentType());
}
@Test
public void testListViaClass()
{
TypeFactory tf = defaultTypeFactory();
JavaType t = tf.constructType(LongList.class);
JavaType type = (CollectionType) t;
assertSame(LongList.class, type.getRawClass());
assertEquals(tf.constructType(Long.class), type.getContentType());
}
@Test
public void testGeneric()
{
TypeFactory tf = defaultTypeFactory();
// First, via simple sub-class
JavaType t = tf.constructType(DoubleRange.class);
JavaType rangeParams = t.findSuperType(Range.class);
assertEquals(1, rangeParams.containedTypeCount());
assertEquals(Double.class, rangeParams.containedType(0).getRawClass());
// then using TypeRef
t = tf.constructType(new TypeReference<DoubleRange>() { });
rangeParams = t.findSuperType(Range.class);
assertEquals(1, rangeParams.containedTypeCount());
assertEquals(Double.class, rangeParams.containedType(0).getRawClass());
}
/*
/**********************************************************
/* Unit tests: wildcard bound resolution [databind#5285]
/**********************************************************
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = EmailSettings.class, name = "EMAIL"),
@JsonSubTypes.Type(value = PhoneSettings.class, name = "PHONE")
})
interface Settings { }
record EmailSettings(String email) implements Settings { }
record PhoneSettings(String phoneNumber) implements Settings { }
record MessageWrapper<T extends Settings>(T settings, String message) { }
// Container to obtain ParameterizedType via field reflection
static class Holder5285 {
MessageWrapper<?> wildcardWrapper;
MessageWrapper<EmailSettings> specificWrapper;
}
static class Box5285<T extends Number> {
public T value;
}
static class BoxHolder5285 {
Box5285<?> wildcardBox;
}
static class Pair5285<L, R> {
public L left;
public R right;
}
@SuppressWarnings("unused")
static class PairHolder5285 {
Pair5285<String, ?> wildcardPair;
}
private final ObjectMapper MAPPER = newJsonMapper();
// Core issue: wildcard type resolution should use declared bound
// [databind#5285]
@Test
void wildcardResolvesToDeclaredBound() throws Exception {
Type genericType = Holder5285.class.getDeclaredField("wildcardWrapper").getGenericType();
JavaType jacksonType = MAPPER.constructType(genericType);
// Should resolve to MessageWrapper<Settings>, NOT MessageWrapper<Object>
assertEquals(MessageWrapper.class, jacksonType.getRawClass());
assertEquals(1, jacksonType.containedTypeCount());
assertEquals(Settings.class, jacksonType.containedType(0).getRawClass());
}
// [databind#5285]
@Test
void specificTypeUnchanged() throws Exception {
Type genericType = Holder5285.class.getDeclaredField("specificWrapper").getGenericType();
JavaType jacksonType = MAPPER.constructType(genericType);
assertEquals(MessageWrapper.class, jacksonType.getRawClass());
assertEquals(1, jacksonType.containedTypeCount());
assertEquals(EmailSettings.class, jacksonType.containedType(0).getRawClass());
}
// [databind#5285]
@Test
void serializationPreservesTypeInfo() throws Exception {
MessageWrapper<EmailSettings> wrapper = new MessageWrapper<>(
new EmailSettings("me@me.com"), "Sample Message");
Type genericType = Holder5285.class.getDeclaredField("wildcardWrapper").getGenericType();
JavaType jacksonType = MAPPER.constructType(genericType);
String json = MAPPER.writerFor(jacksonType).writeValueAsString(wrapper);
assertTrue(json.contains("\"type\":\"EMAIL\""),
"JSON should contain type discriminator, got: " + json);
assertTrue(json.contains("\"email\":\"me@me.com\""),
"JSON should contain email field, got: " + json);
}
// [databind#5285]
@Test
void wildcardResolvesToNumberBound() throws Exception {
Type genericType = BoxHolder5285.class.getDeclaredField("wildcardBox").getGenericType();
JavaType jacksonType = MAPPER.constructType(genericType);
assertEquals(Box5285.class, jacksonType.getRawClass());
assertEquals(1, jacksonType.containedTypeCount());
assertEquals(Number.class, jacksonType.containedType(0).getRawClass());
}
// [databind#5285]
@Test
void unboundedTypeParamStaysObject() throws Exception {
Type genericType = PairHolder5285.class.getDeclaredField("wildcardPair").getGenericType();
JavaType jacksonType = MAPPER.constructType(genericType);
assertEquals(Pair5285.class, jacksonType.getRawClass());
assertEquals(2, jacksonType.containedTypeCount());
assertEquals(String.class, jacksonType.containedType(0).getRawClass());
// R has no explicit bound, so ? should stay as Object
assertEquals(Object.class, jacksonType.containedType(1).getRawClass());
}
}