Generation of metadata for configuration properties doesn't work work with immutable var Kotlin properties.

Module: spring-boot-configuration-processor Version: 2.2.0.M4 Build system: Maven Language: Kotlin Related to: #8762

Property class:

@ConfigurationProperties(prefix = "spring.pulsar")
data class PulsarProperties(      
   val serviceUrl: String = "pulsar://localhost:6650", 
   val listenerThreads: Int = 1,
   ...
)

Maven plugin setup:

            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>kapt</id>
                        <goals>
                            <goal>kapt</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <sourceDir>src/main/kotlin</sourceDir>
                            </sourceDirs>
                            <output></output>
                            <annotationProcessorPaths>
                                <annotationProcessorPath>
                                    <groupId>org.springframework.boot</groupId>
                                    <artifactId>spring-boot-configuration-processor</artifactId>
                                    <version>2.2.0.M4</version>
                                </annotationProcessorPath>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <args>
                        <arg>-Xjsr305=strict</arg>
                    </args>
                    <compilerPlugins>
                        <plugin>spring</plugin>
                    </compilerPlugins>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-allopen</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>

Generated spring-configuration.metadata.json:

{
  "groups": [
    {
      "name": "spring.pulsar",
      "type": "sample.config.PulsarProperties",
      "sourceType": "sample.config.PulsarProperties"
    }
  ],
  "properties": [],
  "hints": []
}

... so it's missing properties completely.

In case that property is specified outside of the constructor (lateinit var property - like before #8762 was implemented) then metadata is generated properly.

In documentation there're mentioned some features are not working with kapt but I do believe this should work (and in case of immutable props also with defaults).

Comment From: snicoll

Defaults definitely don't work as KAPT does not generate that information at all, see #15397. I am surprised there is the creation of a group but no properties. Can you please move that to a small sample I can run myself?

Comment From: To-da

@snicoll sample: https://github.com/To-da/spring-boot-config-md-kotlin-immutable-props

observations: in case that I specify default value (by Kotlin way - not by Spring annotation) for the property with type String the metadata are not generated. For Int property it's ok (known issue is that default value is not retrieved from assigned value due to KAPT limitations). In case that there's String property without assigned default value and after that second on it works.

It definitely needs more detailed tests.

Comment From: To-da

workaround - use @org.springframework.boot.context.properties.bind.DefaultValue annotation and do not use Kotlin default values. Example:

@ConfigurationProperties(prefix = "spring.pulsar")
data class PulsarProperties(
        /**
         *  Note that pulsar client also support reloadable [org.apache.pulsar.client.api.ServiceUrlProvider] interface
         *  to dynamically provide a service URL.
         *  It can be coma separated value in case of non-kubernetes deployment
         */
        @DefaultValue("pulsar://localhost:6650")
        val serviceUrl: String,

        /**
         * Thread pool used to manage the TCP connections with brokers.
         * If you're producing/consuming across many topics, you'll most likely be
         * interacting with multiple brokers and thus have multiple TCP connections opened.
         * Increasing the ioThreads count might remove the "single thread bottleneck",
         * though it would only be effective if such bottleneck is indeed present
         * (most of the time it will not be the case...).
         * You can check the CPU utilization in your consumer process,
         * across all threads, to see if there's any thread approaching 100% (of a single CPU core).
         */
        @DefaultValue("1")
        val ioThreads: Int,

        /**
         * Thread pool size when you are using the message listener in the consumer.
         * Typically this is the thread-pool used by application to process the messages
         * (unless it hops to a different thread). It might make sense to increase
         * the threads count here if the app processing is reaching the 1 CPU core limit.
         */
        @DefaultValue("1")
        val listenerThreads: Int = 1
)
{
  "groups": [
    {
      "name": "spring.pulsar",
      "type": "sample.config.PulsarProperties",
      "sourceType": "sample.config.PulsarProperties"
    }
  ],
  "properties": [
    {
      "name": "spring.pulsar.io-threads",
      "type": "java.lang.Integer",
      "description": "Thread pool used to manage the TCP connections with brokers. If you're producing\/consuming across many topics, you'll most likely be interacting with multiple brokers and thus have multiple TCP connections opened. Increasing the ioThreads count might remove the \"single thread bottleneck\", though it would only be effective if such bottleneck is indeed present (most of the time it will not be the case...). You can check the CPU utilization in your consumer process, across all threads, to see if there's any thread approaching 100% (of a single CPU core).",
      "sourceType": "sample.config.PulsarProperties",
      "defaultValue": 1
    },
    {
      "name": "spring.pulsar.listener-threads",
      "type": "java.lang.Integer",
      "description": "Thread pool size when you are using the message listener in the consumer. Typically this is the thread-pool used by application to process the messages (unless it hops to a different thread). It might make sense to increase the threads count here if the app processing is reaching the 1 CPU core limit.",
      "sourceType": "sample.config.PulsarProperties",
      "defaultValue": 1
    },
    {
      "name": "spring.pulsar.service-url",
      "type": "java.lang.String",
      "description": "Note that pulsar client also support reloadable [org.apache.pulsar.client.api.ServiceUrlProvider] interface to dynamically provide a service URL. It can be coma separated value in case of non-kubernetes deployment",
      "sourceType": "sample.config.PulsarProperties",
      "defaultValue": "pulsar:\/\/localhost:6650"
    }
  ],
  "hints": []
}

Comment From: davinkevin

Same problem if you are using nested data class.

You have to add @ConstructorBinding on nested class to be able to have the documentation generated for it.

Comment From: boris1993

You have to add @ConstructorBinding on nested class to be able to have the documentation generated for it.

This annotation is not necessary for the nested data class, but the nested data class must be inside the outer class like this:

@ConstructorBinding
@ConfigurationProperties(“credentials”)
data class Credentials(
    val someClient: Credential
) {
    data class Credential(
        val clientId: String,
        val clientSecret: String
    )
}

Comment From: snicoll

The sample was built with a milestone of 2.2.0 but adding @ConstructorBinding generates the properties for both the NotOk and Ok POJO so I am going to close this now.