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.

Comment From: jhoeller

Good point! It turns out that this can be optimized even further to a common isAssignableFromResolvedPart check without any hasUnresolvableGenerics guarding at all. I'm revising this for 6.2.3.

Comment From: jhoeller

@tommyk-gears any chance you could give the latest 6.2.3 snapshot a try? In addition to this fix, a couple of other generic type matching refinements have been applied in the meantime.

Comment From: tommyk-gears

@tommyk-gears any chance you could give the latest 6.2.3 snapshot a try? In addition to this fix, a couple of other generic type matching refinements have been applied in the meantime.

For sure. I tried this with version 6.2.3-20250131.143040-22, and it works like a charm. Thanks for quick response and fix @jhoeller!