title: Why is Spring @ConstructorBinding not binding for Kotlin @ConfigurationProperties class?
body: <p>I have a Spring auto configuration library I'm developer for Swagger. It's written in Kotlin using Spring Boot <code>2.2.6</code>.</p>
  
  <p>My main auto configuration is defined as:</p>
  
  <pre class="lang-kotlin prettyprint-override"><code>package io.opengood.autoconfig.swagger
  
  import org.apache.commons.lang3.StringUtils
  import org.slf4j.LoggerFactory.getLogger
  import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
  import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
  import org.springframework.boot.context.properties.EnableConfigurationProperties
  import org.springframework.context.annotation.Bean
  import org.springframework.context.annotation.Configuration
  import springfox.documentation.builders.AuthorizationCodeGrantBuilder
  import springfox.documentation.builders.OAuthBuilder
  import springfox.documentation.builders.PathSelectors
  import springfox.documentation.service.*
  import springfox.documentation.spi.DocumentationType
  import springfox.documentation.spi.service.contexts.SecurityContext
  import springfox.documentation.spring.web.plugins.Docket
  import springfox.documentation.swagger.web.SecurityConfiguration
  import springfox.documentation.swagger.web.SecurityConfigurationBuilder
  import springfox.documentation.swagger2.annotations.EnableSwagger2
  import java.time.LocalDate
  import java.time.LocalDateTime
  import java.time.LocalTime
  import java.sql.Date as SqlDate
  import java.sql.Time as SqlTime
  import java.util.Date as UtilDate
  
  @Configuration
  @ConditionalOnProperty("swagger.enabled")
  @EnableConfigurationProperties(value = [SwaggerProperties::class, OAuth2Properties::class])
  @EnableSwagger2
  class SwaggerAutoConfiguration(
      val swaggerProperties: SwaggerProperties = SwaggerProperties(),
      val swaggerVersion: SwaggerVersion = DefaultSwaggerVersion(),
      val oAuth2Properties: OAuth2Properties = OAuth2Properties()
  ) {
      val paths = swaggerProperties.paths
          .takeIf { !it.isNullOrEmpty() }
          .let { it?.joinToString(",") } ?: SwaggerProperties.DEFAULT_PATH
      val version = swaggerVersion.version
          .takeIf { it.isNotBlank() } ?: swaggerProperties.version
      val authUri = oAuth2Properties.resource.authorizationServerUri
          .takeIf { it.isNotBlank() } ?: OAuth2Properties.DEFAULT_AUTH_URI
      val tokenUri = oAuth2Properties.tokenUri
          .takeIf { it.isNotBlank() } ?: OAuth2Properties.DEFAULT_TOKEN_URI
  
      @Bean
      fun productApi(): Docket {
          log.info("Setup Swagger product configuration")
          val productApi = Docket(DocumentationType.SWAGGER_2)
              .groupName(swaggerProperties.groupName)
              .directModelSubstitute(LocalDateTime::class.java, UtilDate::class.java)
              .directModelSubstitute(LocalDate::class.java, SqlDate::class.java)
              .directModelSubstitute(LocalTime::class.java, SqlTime::class.java)
              .apiInfo(apiInfo())
              .select()
              .paths(PathSelectors.regex(paths))
              .build()
  
          if (oAuth2Properties.enabled &amp;&amp; !authUri.contains("localhost")) {
              productApi.securitySchemes(listOf(securitySchemes()))
              productApi.securityContexts(listOf(securityContext()))
          }
          return productApi
      }
  
      @Bean
      fun apiInfo(): ApiInfo {
          log.info("Setup Swagger API configuration")
          return ApiInfo(
              swaggerProperties.title,
              swaggerProperties.description,
              version,
              swaggerProperties.termsOfServiceUrl,
              Contact(
                  swaggerProperties.contact.name,
                  swaggerProperties.contact.url,
                  swaggerProperties.contact.email),
              swaggerProperties.license.type,
              swaggerProperties.license.url,
              listOf())
      }
  
      @Bean
      @ConditionalOnProperty("swagger.security.oauth2.enabled")
      fun securityInfo(): SecurityConfiguration {
          log.info("Setup Swagger security configuration")
          return if (OAuth2Properties.GrantType.CLIENT_CREDENTIALS == oAuth2Properties.grantType) {
              SecurityConfigurationBuilder.builder()
                  .clientId(StringUtils.EMPTY)
                  .clientSecret(StringUtils.EMPTY)
                  .scopeSeparator(" ")
                  .build()
          } else {
              SecurityConfigurationBuilder.builder()
                  .useBasicAuthenticationWithAccessCodeGrant(true)
                  .build()
          }
      }
  
      private fun securitySchemes(): SecurityScheme {
          return if (OAuth2Properties.GrantType.CLIENT_CREDENTIALS == oAuth2Properties.grantType) {
              OAuthBuilder()
                  .name(SECURITY_REFERENCE_NAME)
                  .grantTypes(listOf(ClientCredentialsGrant(authUri)))
                  .scopes(scopes())
                  .build()
          } else {
              OAuthBuilder()
                  .name(SECURITY_REFERENCE_NAME)
                  .grantTypes(listOf(AuthorizationCodeGrantBuilder()
                      .tokenEndpoint(TokenEndpoint(tokenUri, TOKEN_NAME))
                      .tokenRequestEndpoint(TokenRequestEndpoint(authUri, "", ""))
                      .build()))
                  .scopes(scopes())
                  .build()
          }
      }
  
      private fun securityContext(): SecurityContext {
          return SecurityContext.builder()
              .securityReferences(listOf(SecurityReference(SECURITY_REFERENCE_NAME, scopes().toTypedArray())))
              .forPaths(PathSelectors.regex(paths))
              .build()
      }
  
      private fun scopes(): List&lt;AuthorizationScope&gt; {
          return oAuth2Properties.client.scopes
              .takeIf { it.isNotEmpty() }
              .let { it?.values?.map { s -&gt; AuthorizationScope(s, "") } }
              ?: emptyList()
      }
  
      companion object {
          const val SECURITY_REFERENCE_NAME = "spring_oauth2"
          const val TOKEN_NAME = "oauth2_token"
  
          @Suppress("JAVA_CLASS_ON_COMPANION")
          @JvmStatic
          private val log = getLogger(javaClass.enclosingClass)
      }
  }
  </code></pre>
  
  <p>I have several classes, see main one below, that are injected into the above class as beans using <code>@ConfigurationProperties</code>. I wanted to use the new <code>@ConstructorBinding</code> to remove the ugly <code>lateint var</code> from my main auto config class.</p>
  
  <pre class="lang-kotlin prettyprint-override"><code>package io.opengood.autoconfig.swagger
  
  import org.springframework.boot.context.properties.ConfigurationProperties
  import org.springframework.boot.context.properties.ConstructorBinding
  
  @ConfigurationProperties(prefix = "swagger")
  @ConstructorBinding
  data class SwaggerProperties(
      val enabled: Boolean = true,
      val groupName: String = "",
      val paths: List&lt;String&gt; = listOf(DEFAULT_PATH),
      val title: String = "",
      val description: String = "",
      val version: String = "",
      val termsOfServiceUrl: String = "",
      val contact: Contact = Contact(),
      val license: License = License()
  ) {
      @ConstructorBinding
      data class Contact(
          val name: String = "",
          val url: String = "",
          val email: String = ""
      )
  
      @ConstructorBinding
      data class License(
          val type: String = "",
          val url: String = ""
      )
  
      companion object {
          const val DEFAULT_PATH = ".*"
      }
  }
  </code></pre>
  
  <p>Source code is stored in my GitHub repo at <a href="https://github.com/opengoodio/swagger-auto-configuration" rel="nofollow noreferrer">https://github.com/opengoodio/swagger-auto-configuration</a>.</p>
  
  <p>The main auto config project is under <code>lib/src/main/kotlin/io/opengood/autoconfig/swagger</code>. </p>
  
  <p>I have another project <code>test-app</code> that has a test class <code>test-app/src/test/kotlin/io/opengood/autoconfig/swagger/app</code> called <code>AccessSwaggerTest</code>:</p>
  
  <pre class="lang-kotlin prettyprint-override"><code>package io.opengood.autoconfig.swagger.app
  
  import org.junit.jupiter.api.Test
  import org.junit.jupiter.api.extension.ExtendWith
  import org.springframework.beans.factory.annotation.Autowired
  import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
  import org.springframework.boot.test.context.SpringBootTest
  import org.springframework.test.context.junit.jupiter.SpringExtension
  import org.springframework.test.web.servlet.MockMvc
  import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
  import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
  
  @SpringBootTest(classes = [SwaggerTestApplication::class])
  @ExtendWith(SpringExtension::class)
  @AutoConfigureMockMvc
  class AccessSwaggerTest {
  
      @Autowired
      lateinit var mockMvc: MockMvc
  
      @Test
      fun `swagger UI endpoint is accessible`() {
          mockMvc.perform(get("/swagger-ui.html"))
              .andExpect(status().is2xxSuccessful)
              .andReturn();
      }
  
      @Test
      fun `swagger API docs endpoint is accessible`() {
          mockMvc.perform(get("/v2/api-docs?group=test-group"))
              .andExpect(status().is2xxSuccessful)
              .andReturn();
      }
  }
  </code></pre>
  
  <p>If you run the first test, it fails with:</p>
  
  <pre><code>java.lang.IllegalStateException: Failed to load ApplicationContext
  
      at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
      at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:123)
      at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190)
      at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132)
      at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:244)
      at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:98)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$5(ClassBasedTestDescriptor.java:337)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:342)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$6(ClassBasedTestDescriptor.java:337)
      at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
      at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
      at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1654)
      at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
      at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
      at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312)
      at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735)
      at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734)
      at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:336)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:259)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$2(ClassBasedTestDescriptor.java:252)
      at java.base/java.util.Optional.orElseGet(Optional.java:369)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$3(ClassBasedTestDescriptor.java:251)
      at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:29)
      at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:106)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:105)
      at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:69)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$1(NodeTestTask.java:107)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:107)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:75)
      at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
      at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
      at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
      at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
      at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
      at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
      at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
      at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
      at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
      at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
      at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
      at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
      at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
      at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
      at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
      at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
      at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
      at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
  Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'io.opengood.autoconfig.swagger.OAuth2Properties': @EnableConfigurationProperties or @ConfigurationPropertiesScan must be used to add @ConstructorBinding type io.opengood.autoconfig.swagger.OAuth2Properties
      at org.springframework.boot.context.properties.ConfigurationPropertiesBeanDefinitionValidator.validate(ConfigurationPropertiesBeanDefinitionValidator.java:66)
      at org.springframework.boot.context.properties.ConfigurationPropertiesBeanDefinitionValidator.postProcessBeanFactory(ConfigurationPropertiesBeanDefinitionValidator.java:45)
      at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:286)
      at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:174)
      at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:706)
      at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532)
      at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
      at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
      at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
      at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:126)
      at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
      at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
      ... 63 more
  </code></pre>
  
  <p>If I boot run the simple app, it fails with a similar error.</p>
  
  <p>The main auto config has <code>@EnableConfigurationProperties(value = [SwaggerProperties::class, OAuth2Properties::class])</code> as the error suggests but still fails. I tried the <code>@ConfigurationPropertiesScan</code> on the <code>test-app</code> main class but same error.</p>
  
  <p>I have searched for solutions over the past few months and cannot find a solid reason why this is occurring. </p>
  
  <p>What is causing the <code>@ConstructorBinding</code> to not bind correctly?</p>