I tried both ways how to use nested records as configuration properties. In non of them validation works. I have in the same project configuration with just one record without nesting and that works.

Examples

@Validated
@ConfigurationProperties(prefix = "client")
public record ClientProperties(@NestedConfigurationProperty ServiceOne serviceOne) {
}

@Validated
record ServiceOne(@NotBlank String url, @Email String email) {
}
@Validated
@ConfigurationProperties(prefix = "client")
public record ClientProperties(ServiceOne serviceOne) {

    @Validated
    public record ServiceOne(@NotBlank String url, @Email String email) {
    }

}

Comment From: wilkinsona

Thanks for the report. Unfortunately, it doesn't provide enough information for us to diagnose the problem. We need to know the version of Spring Boot that you're using as well as the client.service-one.* properties that you're trying to bind. The properties are important as, depending on what they are, a ServiceOne instance may not even be created. Please provide this information to us in the form of a complete yet minimal sample that reproduces the problem you have described. You can share it with us by pushing it to a separate repository on GitHub or by zipping it up and attaching it to this issue.

Comment From: michalgebauer

I'm using Spring Boot 3.2.1, all properties are actually null (not provided). See my demo project I created just for the purpose of this issue: https://github.com/michalgebauer/nested-records-validation/blob/main/src/main/java/com/example/demo/DemoApplication.java

I have similar problems with nested properties even if I use classes and not records.

Comment From: wilkinsona

Thanks. The behaviour you have described is to be expected. With no properties to bind to an instance of ServiceOne, no instance of it is created and, therefore, there's nothing to validate. If you want to prohibit this, you can annotate the record component with @NotNull:

@Validated
@ConfigurationProperties(prefix = "client")
record ClientProperties(@NotNull ServiceOne serviceOne) {

    public record ServiceOne(@NotBlank String url, @Email String email) {
    }

}

With this in place, the app will fail to start due to a validation failure:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'client' to com.example.demo.ClientProperties failed:

    Property: client.serviceOne
    Value: "null"
    Reason: must not be null

Alternatively, you can annotate it with both @Valid and @DefaultValue:

@Validated
@ConfigurationProperties(prefix = "client")
record ClientProperties(@DefaultValue @Valid ServiceOne serviceOne) {

    public record ServiceOne(@NotBlank String url, @Email String email) {
    }

}

This will cause the binder to create a default ServiceOne instance with null url and email components. It too will fail validation when the app starts but with an arguably better error message:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'client' to com.example.demo.ClientProperties failed:

    Property: client.serviceOne.url
    Value: "null"
    Reason: must not be blank

Comment From: michalgebauer

Thank you 🙌