Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GenericConversionService cannot find a converter when converting to a Kotlin list of maps #34535

Closed
dmitrysulman opened this issue Mar 4, 2025 · 2 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: regression A bug that is also a regression
Milestone

Comments

@dmitrysulman
Copy link
Contributor

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>>
    }
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 4, 2025
@jhoeller jhoeller added type: regression A bug that is also a regression in: core Issues in core modules (aop, beans, core, context, expression) and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Mar 5, 2025
@jhoeller jhoeller added this to the 6.2.4 milestone Mar 5, 2025
@sdeleuze
Copy link
Contributor

sdeleuze commented Mar 5, 2025

I think we are hitting here some behavior previously discussed in #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>)?

@sdeleuze sdeleuze added the status: waiting-for-feedback We need additional information before we can continue label Mar 5, 2025
@dmitrysulman
Copy link
Contributor Author

@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));
        }
    }
}

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Mar 5, 2025
@jhoeller jhoeller self-assigned this Mar 5, 2025
@sbrannen sbrannen removed the status: feedback-provided Feedback has been provided label Mar 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: regression A bug that is also a regression
Projects
None yet
Development

No branches or pull requests

5 participants