springspring-bootconfigurationpropertiesjava-record

Spring calls noargs constructor on java record for @ConfigurationProperties


I'm pulling my hair out here. I want to use a Java record for my @ConfigurationProperties, providing default values to unspecified config properties. Here is a very simple example:

@ConfigurationProperties(prefix = "myconfig")
public record LoggingProperties (
    String whatever,
    String somethingToDefault
) {
    
    public LoggingProperties(String whatever, String somethingToDefault) {
        this.whatever = whatever;
        this.somethingToDefault = somethingToDefault;
    }

    public LoggingProperties(String whatever) {
        this(whatever, "whatever was specified, but not somethingToDefault");
    }

    public LoggingProperties() {
        this("neither was specified", "neither was specified");
    }
}

It seems, if I declare a noargs constructor, spring always calls that, regardless of what I actually have in my config file (application.yml)

The above will yield an instance, that when logged shows: LoggingProperties[whatever=neither was specified, somethingToDefault=neither was specified], despite the fact that my config WAS specified.

If I delete the no-args constructor, I get an exception about No default constructor found;

If I add @ConstructorBinding to the allargs constructor I get: LoggingProperties[whatever=value from file, somethingToDefault=null]. i.e. it just called the annotated constructor, ignoring the one with 1 arg (despite that prop being declared in my yml).

I'm at a loss... Is this even possible?

EDIT: in my application.yml I have:

myconfig:
  whatever: "value from file"

Solution

  • Praise the lord for documentation (it pays to read it)

    https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.external-config.typesafe-configuration-properties.constructor-binding

    Default values can be specified using @DefaultValue on a constructor parameter or, when using Java 16 or later, a record component. The conversion service will be applied to coerce the String value to the target type of a missing property.

    So it seems I can skip the constructor mess, and just annotate the record fields with a @DefaultValue(value = "whatever default"), like so:

    @ConfigurationProperties(prefix = "someprefix")
    @ConstructorBinding
    public record MyRecord (
        @DefaultValue(value = "true")
        boolean someProperty,
    ) {}