Note: This issue is caused by the solution to the very similar issue #33409 (see below) --> I tried to write this description in the same style.
We are in the process of migrating to Spring Boot 3.0 and we noticed that nested record properties with default values can no longer be bound.
Given a configuration class...
@Configuration
@ConfigurationProperties(prefix = "example")
public class ExampleProperties {
@NestedConfigurationProperty
private NestedProperty nested = new NestedProperty("Default", "default");
public NestedProperty getNested() {
return nested;
}
public void setNested(NestedProperty nested) {
this.nested = nested;
}
}
... and the NestedProperty, which is a record:
public record NestedProperty(String name, String value) {}
Test (using Spock):
@SpringBootTest(properties = ['example.nested.name=testName', 'example.nested.value=testValue'])
class DemoApplicationSpec extends Specification {
@Inject
ExampleProperties exampleProperties
def 'TestProperties'() {
expect:
exampleProperties.nested.name() == 'testName'
exampleProperties.nested.value() == 'testValue'
}
}
The test runs successfully using Spring Boot 2.7.8, but fails on 3.0.2. The reason is this if-statement that was introduced by @philwebb with this commit to fix gh-33409. The idea for this change was to force setter binding instead of constructor binding for JavaBeans if the target value already exists (by throwing away the "deduced bind constructor"), to match the JavaBeans binding behavior in Spring 2.x.
Unfortunately, this change does not really make sense for records, as records are immutable and hence setter binding cannot be used. I would expect Spring 3.x to distinguish between the two use-cases (setter binding for JavaBeans vs. constructor binding for records) just like Spring 2.x did.
Workaround until this is fixed
As a workaround, using constructor binding for records can be enforced using @ConstructorBinding on a (theoretically redundant) additional constructor:
public record NestedProperty(String name, String value) {
@ConstructorBinding
public NestedProperty(String name, String value) {
this.name = name;
this.value = value;
}
}
Now, the binding constructor is not considered "deduced" any more and is used as expected.
Comment From: philwebb
Thanks for the detailed analysis, it really helped!
Comment From: winne42
Wow, that was blazingly fast! Thanks so much for your work, @philwebb ! ♥