I'm upgrading my application from 2.4.10 to 2.5.6 and one of my custom Converters does not get picked up anymore since a specific static factory method (from) is present on the targetType.

After debugging this issue, it looks like in 2.5, the ObjectToObjectConverter wins, and gets called before my custom Converter is picked up.

Even though the return type of the factory method does not fit the targetType!

In my case the factory method returns an Optional.

public class Data {
    final String data;

    public Data(String data) {
        this.data = data;
    }

    public static Optional<Data> from(String s) {
        return Optional.of(new Data(s));
    }
}

while my Properties expects a Data.

In spring-boot 2.4.x this works for me.

The converter gets selected by the Binder, and my property is bound to the properties class.

In spring boot 2.5, the exact same code base fails with ConverterNotFoundException

The resulting error looks like this:

Failed to bind properties under 'my-data' to app.Data:

    Property: my-data
    Value: hello
    Origin: class path resource [application.properties] - 1:8
    Reason: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.util.Optional<?>] to type [app.Data]

In my opinion this is a bug in the priority of the generic ObjectToObjectConverter (not taking into account the targetType) and user-defined custom converters!

Reproduce

A small reproducer is here: https://github.com/DieBauer/spring-boot-propertiesbinding

I have a properties class that gets instantiated with a custom data type like so:

myData=hello

and the corresponding properties class.

@ConfigurationProperties
public class DataProperties {
    Data myData;

    public void setMyData(Data myData) {
        this.myData = myData;
    }

    public Data getMyData() {
        return myData;
    }
}

To make this work, I have a custom converter

public class DataConverter implements Converter<String, Data> {
    @Override
    public Data convert(String source) {
        return new Data(source);
    }
}

Which is annotated like this in my Configuration class.

    @Bean
    @ConfigurationPropertiesBinding
    public DataConverter converter() {
        return new DataConverter();
    }

Potential workarounds

Register the converter manually

I can register my Converter directly in the conversionService bean, following https://github.com/spring-projects/spring-boot/issues/26294 and the linked SO question

like this:

@Bean
public ConversionService conversionService(@Autowired Set<Converter> converters) {
    ConversionServiceFactoryBean factory = new ConversionServiceFactoryBean();
    factory.setConverters(converters);
    factory.afterPropertiesSet();
    return factory.getObject();
}

rendering the @ConfigurationPropertiesBinding useless.

Rename my static factory method

When the static factory method does not match any of the three terms in the ObjectToObjectConverter (valueOf, of, from) my converter gets picked up.

Add a converter as per suggestion of the error message

Adding a converter of Optional<?> (or whatever the return type of the static factory method is) to your custom type might work.

Comment From: wilkinsona

Thanks for the sample. I've reproduced the behaviour that you've described. It appears to be a regression introduced in 2.5.0-RC1 as your sample works fine with 2.5.0-M3. I've yet to verify this, but I suspect that https://github.com/spring-projects/spring-boot/commit/51c3e180113d181e4713a7adafa6636041686e9c may be the cause.

Comment From: philwebb

I've added a fix in Boot, but I think this is really a framework bug. I've opened https://github.com/spring-projects/spring-framework/issues/28609.