Skip to content

GenericConversionService finds wrong converter for partially unresolvable generic types #34298

Closed
@tommyk-gears

Description

@tommyk-gears

If there are multiple converters for similar types, but with different generic type arguments that are partially unresolvable - then the generic type arguments are effectively ignored and as a result one might get a non-matching converter.

The issue can be demonstrated with this test:

class GenericConversionServiceTests {
	
	private final GenericConversionService conversionService = new GenericConversionService();

	@Test
	void stringListToListOfSubClassesOfUnboundGenericClass() throws Exception {
		conversionService.addConverter(new StringListToAListConverter());
		conversionService.addConverter(new StringListToBListConverter());

		List<ARaw> aList = (List<ARaw>) conversionService.convert(List.of("foo"),
				TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)),
				TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(ARaw.class)));
		assertThat(aList).allMatch(e -> e instanceof ARaw);

		List<BRaw> bList = (List<BRaw>) conversionService.convert(List.of("foo"),
				TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)),
				TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(BRaw.class)));
		assertThat(bList).allMatch(e -> e instanceof BRaw);
	}

	public class ARaw extends GenericBaseClass {
	}

	public class BRaw extends GenericBaseClass {
	}

	public class GenericBaseClass<T> {
	}

	public class StringListToAListConverter implements Converter<List<String>, List<ARaw>> {

		@Override
		public List<ARaw> convert(List<String> source) {
			return List.of(new ARaw());
		}
	}

	public class StringListToBListConverter implements Converter<List<String>, List<BRaw>> {

		@Override
		public List<BRaw> convert(List<String> source) {
			return List.of(new BRaw());
		}
	}
}

As far as I understand this could be fixed by a small change in GenericConversionService.ConverterAdapter#matches to take the resolvable part into account even if the type is not completely unresolvable. I.e. if the resolvable part does not match, then this converter does not match.

@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
	// Check raw type first...
	if (this.typeInfo.getTargetType() != targetType.getObjectType()) {
		return false;
	}
	// Full check for complex generic type match required?
	ResolvableType rt = targetType.getResolvableType();
	if (!(rt.getType() instanceof Class) && !rt.isAssignableFrom(this.targetType) &&
-					!this.targetType.hasUnresolvableGenerics()) {
+					(!this.targetType.hasUnresolvableGenerics() || !rt.isAssignableFromResolvedPart(this.targetType))) {
		return false;
	}
	return !(this.converter instanceof ConditionalConverter conditionalConverter) ||
			conditionalConverter.matches(sourceType, targetType);
}

The concrete use case I have is converting a List<A> to List<B> where B is a generated protobuf class which has partially unresolvable generics due to this. Also, I cannot use a plain Converter<A,B> because the converter needs access to the full source list to produce the target list - so I the natural solution is to have a Converter<List<A>, List<B>>, but it does not work since I have multiple such converters with same source type and different target types.
One possible workaround would be to wrap the Lists in distinct wrapper classes during conversion, but I think it would be better to just make GenericConversionService consider partially resolvable generics.

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)type: enhancementA general enhancement

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions