Skip to content

TypeFactory.constructType() does not take TypeBindings correctly #2796

Closed
@TheSpiritXIII

Description

@TheSpiritXIII

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 extending TypeFactory and overriding constructParametricType and adding the refining as seen in _fromClass, as well as the with 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions