Having a simple Kotlin Spring Boot application (link), adding some data class as configuration properties class causes the native image to fail due to some reflection issues:

@ConfigurationProperties("bar")
data class BarProperties(val name: BarName)

data class BarName(val bar: String)

Stacktrace:

Caused by: kotlin.reflect.jvm.internal.KotlinReflectionInternalError: Could not compute caller for function: public constructor BarName(bar: kotlin.String) defined in com.example.demo.BarName[DeserializedClassConstructorDescriptor@2cae19c5] (member = null)
        at kotlin.reflect.jvm.internal.KFunctionImpl$caller$2.invoke(KFunctionImpl.kt:88) ~[na:na]
        at kotlin.reflect.jvm.internal.KFunctionImpl$caller$2.invoke(KFunctionImpl.kt:61) ~[na:na]
        at kotlin.reflect.jvm.internal.ReflectProperties$LazyVal.invoke(ReflectProperties.java:63) ~[na:na]
        at kotlin.reflect.jvm.internal.ReflectProperties$Val.getValue(ReflectProperties.java:32) ~[test-kotlin-native:1.7.20-release-201(1.7.20)]
        at kotlin.reflect.jvm.internal.KFunctionImpl.getCaller(KFunctionImpl.kt:61) ~[na:na]
        at kotlin.reflect.jvm.ReflectJvmMapping.getJavaConstructor(ReflectJvmMapping.kt:71) ~[na:na]
        at org.springframework.beans.BeanUtils$KotlinDelegate.findPrimaryConstructor(BeanUtils.java:852) ~[na:na]
        at org.springframework.beans.BeanUtils.findPrimaryConstructor(BeanUtils.java:282) ~[na:na]
        at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.deduceKotlinBindConstructor(DefaultBindConstructorProvider.java:172) ~[na:na]
        at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.getConstructors(DefaultBindConstructorProvider.java:89) ~[na:na]
        at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider.getBindConstructor(DefaultBindConstructorProvider.java:50) ~[na:na]
        at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider.getBindConstructor(DefaultBindConstructorProvider.java:42) ~[na:na]
        at org.springframework.boot.context.properties.bind.ValueObjectBinder$ValueObject.get(ValueObjectBinder.java:190) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.ValueObjectBinder.bind(ValueObjectBinder.java:67) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:472) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.ValueObjectBinder$ConstructorParameter.bind(ValueObjectBinder.java:314) ~[na:na]
        at org.springframework.boot.context.properties.bind.ValueObjectBinder.bind(ValueObjectBinder.java:76) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:332) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bindOrCreate(Binder.java:324) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bindOrCreate(Binder.java:309) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bindOrCreate(ConfigurationPropertiesBinder.java:101) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.ConstructorBound.from(ConstructorBound.java:43) ~[na:na]
        at com.example.demo.BarProperties__BeanDefinitions.getBarPropertiesInstance(BarProperties__BeanDefinitions.java:24) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1225) ~[test-kotlin-native:6.0.0-RC4]
        ... 48 common frames omitted

Adding the following hint resolve the issue:

class NativeRuntimeHints : RuntimeHintsRegistrar {
    override fun registerHints(hints: RuntimeHints, classLoader: ClassLoader?) {
        hints.reflection().registerType(
            BarName::class.java,
            MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS,
            MemberCategory.INVOKE_DECLARED_CONSTRUCTORS
        )
    }
}

I'm not sure if this is an issue in Spring Boot or Sprig Framework.

Comment From: wilkinsona

Looking at this more closely, the problem is that it's not clear that BarName is a nested configuration property. It should work if the code is structured like this:

@ConfigurationProperties("bar")
data class BarProperties(val name: BarName) {

    data class BarName(val bar: String)

}

It should also be possible to use @NestedConfigurationProperty to indicate that BarName should be treated as if it is nested. However, I don't think that's possible at the moment as @NestedConfigurationProperty can only be used on a field. We may have a similar problem with Java records.

Comment From: wilkinsona

Kotlin's smart enough to apply the @NestedConfigurationProperty to the field that's generated by the compiler. This works:

@ConfigurationProperties("bar")
data class BarProperties(@NestedConfigurationProperty val name: BarName)

data class BarName(val bar: String)

Java records are also OK. You can either nest the records:

record BarProperties(BarName name) {

    record BarName(String bar) {

    }

}

Or use the annotation:

record BarProperties(@NestedConfigurationProperty BarName name) {

}

record BarName(String bar)  {

}

We should add some examples to the documentation.

Comment From: wilkinsona

https://github.com/spring-projects/spring-boot/issues/33239 should be done before this.

Comment From: mhalbritter

FYI: I've done #33239.