When constructor for ConfigurationProperties bean has wildcard parameters, Boot doesn't apply converters, but instead keeps String as-is.

For example, if parameter is Map<String, ? extends List<? extends InetAddress>> (this is how Kotlin translates List<String, Map<InetAddress>> into Java bytecode), Boot will pass into constructor a map of lists of strings, while it should have converted those strings into InetAddresses.

Reproducer:

package net.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.context.ConfigurableApplicationContext;

import java.net.InetAddress;
import java.util.List;
import java.util.Map;

@SpringBootApplication
public class ExampleApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(ExampleApplication.class, args);
        ExampleProperties properties = ctx.getBean(ExampleProperties.class);

        // OK.
        System.out.println(properties.getAddresses1().get("localhost").get(0).getHostAddress());

        // ClassCastException: class java.lang.String cannot be cast to class java.net.InetAddress
        System.out.println(properties.getAddresses2().get("localhost").get(0).getHostAddress());
    }

    @ConstructorBinding
    @ConfigurationProperties("example")
    public static class ExampleProperties {
        private final Map<String, List<InetAddress>> addresses1;
        private final Map<String, ? extends List<? extends InetAddress>> addresses2;

        public ExampleProperties(Map<String, List<InetAddress>> addresses1, Map<String, ? extends List<? extends InetAddress>> addresses2) {
            this.addresses1 = addresses1;
            this.addresses2 = addresses2;
        }

        public Map<String, List<InetAddress>> getAddresses1() {
            return addresses1;
        }

        public Map<String, ? extends List<? extends InetAddress>> getAddresses2() {
            return addresses2;
        }
    }

}
example:
    addresses1:
        localhost:
            -   127.0.0.1
    addresses2:
        localhost:
            -   127.0.0.1

Comment From: snicoll

Thanks for the sample. I've edited your title to clarify the issue is not related to constructor binding (I can reproduce with regular JavaBean accessors as well).

Comment From: mbhave

This is a bug in Spring Framework's ResolvableType class. I'll leave this issue open so that we can add a test for this case in MapBinderTests.

Comment From: philwebb

See https://github.com/spring-projects/spring-framework/pull/24145