Hello 👋 ,

I searched for a similar issue, but didn't find one so I chose to open it… If I've missed it, sorry 😇.

Following modification of @ConstructingBinding in 3.0.0-M1, I had to remove the annotation from our ConfigurationProperties classes:

// @ConstructingBinding
@ConfigurationProperties("podcastserver.externaltools")
data class ExternalTools(
        val ffmpeg: String = "/usr/local/bin/ffmpeg",
        val ffprobe: String = "/usr/local/bin/ffprobe",
        val rtmpdump: String = "/usr/local/bin/rtmpdump",
        val youtubedl: String = "/usr/local/bin/youtube-dl"
)

With this, the application fails to start with the following error

[app] Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
[app] 2022-09-18T15:01:49.280Z ERROR 1 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   :
[app]
[app] ***************************
[app] APPLICATION FAILED TO START
[app] ***************************
[app]
[app] Description:
[app]
[app] Failed to bind properties under 'podcastserver.externaltools' to com.github.davinkevin.podcastserver.service.properties.ExternalTools:
[app]
[app]     Property: podcastserver.externaltools.ffmpeg
[app]     Value: "/opt/ffmpeg/ffmpeg"
[app]     Origin: System Environment Property "PODCASTSERVER_EXTERNALTOOLS_FFMPEG"
[app]     Reason: java.lang.IllegalStateException: No setter found for property: ffmpeg
[app]
[app] Action:
[app]
[app] Update your application's configuration

Because of the Kotlin nature, there is multiple constructors generated to support default values defined. I've tried to use the annotation on the constructor like this

@ConfigurationProperties("podcastserver.externaltools")
data class ExternalTools @ConstructingBinding constructor(
        val ffmpeg: String = "/usr/local/bin/ffmpeg",
        val ffprobe: String = "/usr/local/bin/ffprobe",
        val rtmpdump: String = "/usr/local/bin/rtmpdump",
        val youtubedl: String = "/usr/local/bin/youtube-dl"
)

but the result is not working too, for a different reason

[app] 2022-09-18T14:58:45.978Z ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed
[app]
[app] java.lang.IllegalStateException: com.github.davinkevin.podcastserver.service.properties.ExternalTools declares @ConstructorBinding on a no-args constructor
[app]   at org.springframework.util.Assert.state(Assert.java:97) ~[spring-core-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider$Constructors.findAnnotatedConstructor(ConfigurationPropertiesBindConstructorProvider.java:135) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider$Constructors.getConstructors(ConfigurationPropertiesBindConstructorProvider.java:95) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider.getBindConstructor(ConfigurationPropertiesBindConstructorProvider.java:52) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.context.properties.ConfigurationPropertiesBean$BindMethod.forType(ConfigurationPropertiesBean.java:325) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.createBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:92) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.registerBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:88) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:60) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:54) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
[app]   at org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.registerBeanDefinitions(EnableConfigurationPropertiesRegistrar.java:49) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.context.annotation.ImportBeanDefinitionRegistrar.registerBeanDefinitions(ImportBeanDefinitionRegistrar.java:86) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.lambda$loadBeanDefinitionsFromRegistrars$1(ConfigurationClassBeanDefinitionReader.java:384) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) ~[na:na]
[app]   at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:383) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:156) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:128) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:366) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:262) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:344) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:115) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:755) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:573) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app]   at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:430) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.SpringApplication.run(SpringApplication.java:312) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app]   at com.github.davinkevin.podcastserver.PodcastServerApplicationKt.main(PodcastServerApplication.kt:17) ~[classes/:na]
[app]

The only way to make it working was to use the @ConstructorBinding constructor(…) with no default values… which is something we would like to avoid of course.

Thank you for your help on this subject.

Comment From: wilkinsona

Thanks for the report. It looks like these changes to ConfigurationPropertiesBindConstructorProvider went a little too far as we've lost the custom handling of Kotlin types that allows the primary constructor of a data class to be used for constructor-based binding.

Comment From: kkocel

@wilkinsona which version has this fix? I've tried 3.0.0-RC2 and the issue is still there.

My data class:

@ConfigurationProperties("foo.locale")
data class LocaleProperties @ConstructorBinding constructor(
    val default: String = "en-US",
    val supported: List<String> = listOf()
)

the exception I get:

Caused by: java.lang.IllegalStateException: com.example.LocaleProperties declares @ConstructorBinding on a no-args constructor
    at org.springframework.util.Assert.state(Assert.java:97)
    at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.getConstructorBindingAnnotated(DefaultBindConstructorProvider.java:137)
    at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.getConstructors(DefaultBindConstructorProvider.java:84)
    at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider.getBindConstructor(DefaultBindConstructorProvider.java:50)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBean$BindMethod.get(ConfigurationPropertiesBean.java:327)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.createBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:92)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.registerBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:88)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:60)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:54)
    at java.base/java.lang.Iterable.forEach(Iterable.java:75)
    at org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.registerBeanDefinitions(EnableConfigurationPropertiesRegistrar.java:49)
    at org.springframework.context.annotation.ImportBeanDefinitionRegistrar.registerBeanDefinitions(ImportBeanDefinitionRegistrar.java:86)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.lambda$loadBeanDefinitionsFromRegistrars$1(ConfigurationClassBeanDefinitionReader.java:373)
    at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:372)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:148)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:120)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:409)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:283)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:344)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:115)
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:745)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:565)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:432)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
    at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$3(SpringBootContextLoader.java:134)
    at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:59)
    at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:47)
    at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1386)
    at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:526)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:134)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:105)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:183)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:117)
    ... 88 more

Comment From: wilkinsona

As shown by the milestone to which this issue is assigned, the fix was made in 3.0.0-M5.

I don't think you need @ConstructorBinding. In the problem description, @davinkevin tried that in an attempt to get things to work. If you look at the test in added in https://github.com/spring-projects/spring-boot/commit/6b8575b001450c63fd34a55ae6314141ec7172d9, @ConstructorBinding isn't used.

Comment From: mkosmul

@wilkinsona The issue seems to be back as of 3.0.5. The following data class:

@ConfigurationProperties("some.properties")
data class SomeProperties @ConstructorBinding constructor(val value: String? = null)

causes an exception during Spring context creation:

java.lang.IllegalStateException: Failed to load ApplicationContext for ...
...
Caused by: java.lang.IllegalStateException: pl.allegro.tech.selfservice.newservicesingle.domain.DomainConfiguration declares @ConstructorBinding on a no-args constructor
...

Adding an explicit no-args constructor without the annotation fixes the issue, but is unwieldy (brevity is an important selling point for using data classes for configuration properties, after all).

@ConfigurationProperties("some.properties")
data class SomeProperties @ConstructorBinding constructor(val value: String? = null) {
    constructor() : this(null)
}

Behavior is the same for org.springframework.boot.context.properties.bind.ConstructorBinding and for org.springframework.boot.context.properties.ConstructorBinding.

Comment From: wilkinsona

@mkosmul As per my comment immediately before yours, I don't think you need to use @ConstructorBinding.

Comment From: green-green-avk

Hmm...

As of Spring Boot v3.1.2:

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component

@Component
@ConfigurationProperties("app.someProps")
class SomeProps(
    val someProp: Boolean = true
)
...
Reason: java.lang.IllegalStateException: No setter found for property: some-prop
...

strikes again.

Comment From: philwebb

@green-green-avk Can you please open a new issue for this and provide a complete reproducer that we can run and debug?

Comment From: daliborfilus

For anyone searching for this and discovering this issue via google: @green-green-avk If I understand correctly, you need to remove @Component and add @EnableConfigurationProperties(SomeProps::class) to one of your other configuration components. @Component alone throws No setter found for property messages for me too.

I.e. this works for me on spring boot 3.2.2:

@Configuration
@EnableConfigurationProperties(SomeProps::class)
class CorsConfiguration {
}

@ConfigurationProperties(prefix = "myproject.cors")
data class SomeProps(
    val enabled: Boolean = false,
)

And this doesn't:

@Configuration
//@EnableConfigurationProperties(SomeProps::class)
class CorsConfiguration {
}

@Component
@ConfigurationProperties(prefix = "myproject.cors")
data class SomeProps(
    val enabled: Boolean = false,
)

This version throws:

Failed to bind properties under 'myproject.cors' to ....config.SomeProps:

    Property: myproject.cors.enabled
    Value: "true"
    Origin: class path resource [application.yml] - 13:14
    Reason: java.lang.IllegalStateException: No setter found for property: enabled

Switch to the first version to make it work with val.