javaspringspring-bootjacksonspring-boot-configuration

How can I use Optional values in spring-boot configuration properties?


Using spring-boot v3.2.5 I've got configuration that deserializes to this class:

@Getter
// Intentionally Validated and not jakarata.validation.Valid because Valid does not trigger
// validation for configuration properties, and I don't know why.
@Validated
@ConfigurationProperties(prefix = "pfx")
public class TenantsConfiguration {

  @NotNull private final Map<String, TenantConfiguration> tenants;

  @ConstructorBinding
  public TenantsConfiguration(Map<String, TenantConfiguration> tenants) {
    this.tenants = tenants;
  }

  @Getter
  public static class TenantConfiguration {
      @NotNull private final Optional<String> field1;
      @NotNull private final Optional<String> field2;

      @ConstructorBinding
    public TenantConfiguration(
        Optional<String> field1, Optional<String> field2) {
        this.field1 = field1;
        this.field2 = field2;
      }
  }
}

And a config that looks like:

pfx:
  tenants:
    t1:
      field1: abc

The application fails to start because field2 is null instead of Optional.empty():

Binding to target TenantsConfiguration failed:

    Property: pfx.tenants.t1.field2
    Value: "null"
    Reason: field2 must not be null

During deserialization from api requests, spring handles null -> Optional conversion just fine. I tried converting TenantConfiguration to a record, but that did not change anything. How do I get the normal null -> Optional conversion when I'm loading configuration in spring-boot?


Solution

  • Optional is usually not recommended for ConfigurationProperties. This is a known behaviour which spring-boot marked as 'not a bug, but a feature'.

    Source : https://github.com/spring-projects/spring-boot/issues/21868

    Checkout these comments specifically :

    So a work around for your problem would be :

    @Getter
    public static class TenantConfiguration {
        @NotNull private Optional<String> field1;
        @NotNull private Optional<String> field2;
    
        @ConstructorBinding
        public TenantConfiguration(String field1, String field2) {
          this.field1 = Optional.ofNullable(field1);
          this.field2 = Optional.ofNullable(field2);
        }
    }