Description
Using Jackson version 2.11.0, suppose we want to deserialize a type as Optional<Set<Integer>>
so I construct a JavaType
for it and deserialize.
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Jdk8Module());
final TypeFactory typeFactory = objectMapper.getTypeFactory();
- Option 1:
constructType
:
typeFactory.constructType(Optional.class, TypeBindings.create(Optional.class, new JavaType[] {
typeFactory.constructType(Set.class, TypeBindings.create(Set.class, new JavaType[] {
typeFactory.constructType(Integer.class)
}))
}));
Inside _fromClass
, _typeCache.get(key)
return a cached entry and exits early. This simple type becomes refined with missing bindings (EMPTY_BINDINGS
is passed into _fromClass
, even though I passed explicit bindings) because resultType.getBindings()
is empty. Our end result is a reference type Optional<Object>
.
I deserialize and uh oh! I actually get an Optional<ArrayList>
and I get an exception.
- Option 2:
constructParametricType
:
typeFactory.constructParametricType(Optional.class,
typeFactory.constructParametricType(Set.class,
typeFactory.constructType(Integer.class))
);
We go inside _fromClass
and we do not early exit because the bindings I give are passed directly in. However, there's no refining happening so we end up with with a simple type Optional<Set<Integer>>
which causes deserialization issues because the simple type is being deserialized as if it were a bean.
I deserialize and uh oh!
- Workaround:
I was able to workaround this issue by extendingTypeFactory
and overridingconstructParametricType
and adding the refining as seen in_fromClass
, as well as thewith
builder APIs because otherwise registering a module caused me to revert back to the original TypeFactory class:
public class TypeFactory2 extends TypeFactory {
private static final long serialVersionUID = 6804290482184294993L;
public TypeFactory2() {
super(null);
}
public TypeFactory2(
@Nullable LRUMap<Object, JavaType> typeCache,
@Nullable TypeParser p,
@Nullable TypeModifier[] mods,
@Nullable ClassLoader classLoader
) {
super(typeCache, p, mods, classLoader);
}
@Override
public TypeFactory withModifier(TypeModifier mod) {
LRUMap<Object, JavaType> typeCache = _typeCache;
TypeModifier[] mods;
if (mod == null) { // mostly for unit tests
mods = null;
// 30-Jun-2016, tatu: for some reason expected semantics are to clear cache
// in this case; can't recall why, but keeping the same
typeCache = null;
} else if (_modifiers == null) {
mods = new TypeModifier[] { mod };
// 29-Jul-2019, tatu: Actually I think we better clear cache in this case
// as well to ensure no leakage occurs (see [databind#2395])
typeCache = null;
} else {
// but may keep existing cache otherwise
mods = ArrayBuilders.insertInListNoDup(_modifiers, mod);
}
return new TypeFactory2(typeCache, _parser, mods, _classLoader);
}
@Override
public TypeFactory withClassLoader(ClassLoader classLoader) {
return new TypeFactory2(_typeCache, _parser, _modifiers, classLoader);
}
@Override
public TypeFactory withCache(LRUMap<Object, JavaType> cache) {
return new TypeFactory2(cache, _parser, _modifiers, _classLoader);
}
@Override
public JavaType constructParametricType(Class<?> rawType, JavaType... parameterTypes) {
JavaType resultType = _fromClass(null, rawType, TypeBindings
.create(rawType, parameterTypes));
if (_modifiers != null) {
TypeBindings b = resultType.getBindings();
if (b == null) {
b = EMPTY_BINDINGS;
}
for (TypeModifier mod : _modifiers) {
JavaType t = mod.modifyType(resultType, rawType, b, this);
if (t == null) {
throw new IllegalStateException(String
.format("TypeModifier %s (of type %s) return null for type %s", mod, mod
.getClass()
.getName(), resultType));
}
resultType = t;
}
}
return resultType;
}
}
Then,
objectMapper.setTypeFactory(new TypeFactory2());
If my problem was the API I was calling, please let me know kindly. TypeFactory
is a bit of a minefield with all those deprecated and seemingly un-deprecated methods. :)
Thanks