This test works fine in Spring 6.2.2 but fails in 6.2.3.

It is probably related to this fix: #34298.

package com.example

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.core.convert.TypeDescriptor
import org.springframework.core.convert.converter.Converter
import org.springframework.core.convert.support.GenericConversionService

class GenericConversionServiceTest {

    private val conversionService = GenericConversionService()

    @Test
    fun stringToListOfStringToAnyMapsConverterTest() {
        conversionService.addConverter(StringToListOfStringToAnyMapsConverter)

        val result = conversionService.convert(
            "foo",
            TypeDescriptor.valueOf(String::class.java),
            TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(Map::class.java))
        ) as List<Map<String, Any>>

        assertEquals("foo", result.first()["bar"])
    }
}

object StringToListOfStringToAnyMapsConverter : Converter<String, List<Map<String, Any>>> {
    override fun convert(source: String): List<Map<String, Any>> {
        return listOf(mapOf("bar" to source))
    }
}

Exception:

No converter found capable of converting from type [java.lang.String] to type [java.util.List<java.util.Map<?, ?>>]
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [java.util.List<java.util.Map<?, ?>>]
    at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:294)
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:185)
    at com.medisafe.chp.client.service.GenericConversionServiceTest.stringToListOfStringToAnyMapsConverterTest(GenericConversionServiceTest.kt:17)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

After a brief investigation, we found that this issue may be related to Kotlin declaration-site variance. Kotlin's List is declared with <out E>:

public actual interface List<out E> : Collection<E>

As a result, Converter<String, List<Map<String, Any>>> compiles to something like Converter<String, List<? extends Map<String, ?>>>. The fix introduced in #34298 makes this converter incompatible with the target type List<String, Map<?, ?>>.

As a workaround we replaced Kotlin's List with Java equivalent in the converter declaration. The following converter works correctly:

object StringToListOfStringToAnyMapsConverter : Converter<String, java.util.List<Map<String, Any>>> {
    override fun convert(source: String): java.util.List<Map<String, Any>> {
        return listOf(mapOf("bar" to source)) as java.util.List<Map<String, Any>>
    }
}

Comment From: sdeleuze

I think we are hitting here some behavior previously discussed in https://github.com/spring-projects/spring-framework/issues/22313 with Kotlin List<Foo> generating something like Java List<? extends Foo> which confuses the refined resolution algorithm. Ideally, we should target to relax a bit the generics check to fix this regression without involving Kotlin reflection as the main Kotlin specific point is that collections like List<? extends Foo> are more frequent.

@dmitrysulman Could you please provide a pure Java version of GenericConversionServiceTest (potentially using IDEA decompilation capabilities or writing it manually) that reproduces the regression (using collections like List<? extends Foo>)?

Comment From: dmitrysulman

@sdeleuze sure, here is Java version:

package com.example;

import org.junit.jupiter.api.Test;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.GenericConversionService;

import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

class GenericConversionServiceTest {

    private final GenericConversionService conversionService = new GenericConversionService();

    @Test
    void stringToListOfStringToAnyMapsConverterTest() {
        conversionService.addConverter(new StringToListOfStringToAnyMapsConverter());

        List<Map<String, Object>> result = (List<Map<String, Object>>) conversionService.convert(
                "foo",
                TypeDescriptor.valueOf(String.class),
                TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Map.class))
        );

        assertEquals("foo", result.get(0).get("bar"));
    }


    private static class StringToListOfStringToAnyMapsConverter implements Converter<String, List<? extends Map<String, ?>>> {

        @Override
        public List<? extends Map<String, ?>> convert(String source) {
            return List.of(Map.of("bar", source));
        }
    }
}