The relaxed binding of Configuration Properties in Spring Boot 3.5.0 is broken for records with field names containing an uppercase letter.

Running this:

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({Config.MyProperties.class})
public class Config {

    @ConfigurationProperties("test")
    public record MyProperties(String valueName) {

        public MyProperties(String valueName) {
            this.valueName = Objects.requireNonNull(valueName);
        }
    }
}

@Component
public class MyService {

    public MyService(MyProperties myProperties){
    }
}

@SpringBootApplication(proxyBeanMethods = false)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

while a test_value_name environment variable with value abc is present works in 3.4.6 (and earlier), but fails with:

***************************
APPLICATION FAILED TO START
***************************

Description: Failed to bind properties under 'test' to example.Config$MyProperties:

Reason: org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'test' to example.Config$MyProperties

Action: Update your application's configuration

when using 3.5.0.

See https://github.com/PascalSchumacher/SpringBootConfigurationPropertyBug for running example project.

By the way: Thank you very much for developing/providing Spring Boot!

Comment From: philwebb

This might be related to some caching changes that we made in Spring Boot 3.5, however, I think the format of your environment variable is wrong.

There are some guidelines in the reference docs about the correct format. For your example, the name should be specified as TEST_VALUENAME when it is an environment variable.

Comment From: philwebb

Flagging for team attention to see if we want to treat this as a regression (and possibly unwind the performance improvements) or keep the new 3.5 code.

Comment From: nosan

That worked before 3.5.0 because SpringIterableConfigurationPropertySource delegated environment property resolution to SystemEnvironmentPropertySource, and this behavior is no longer true. See commit.

Version 3.5.0 tries to resolve the following environment keys:

  • TEST_VALUENAME
  • TEST_VALUE_NAME
  • test.value-name

However, with the environment variable test_value_name=some value, version 3.5.0 finds nothing.

3.4.x delegates property resolution to SystemEnvironmentPropertySource, which performs some additional replacements. Specifically, the lookup value test.value-name is converted to test_value_name, which explains why it worked earlier.

Comment From: nosan

@Value("${test.value-name}") works fine. (It is handled by PropertySourcesPlaceholderConfigurer. )

It looks like there is some inconsistency between how Spring Boot resolves properties and Spring Framework. I personally think that it is better to delegate to the core SystemEnvironmentPropertySource to be consistent.

Comment From: wilkinsona

I'm in favor of leaving things as they are as it aligns the implementation with the documentation. Furthermore, the correct format (TEST_VALUENAME) works with both 3.4.x and 3.5.x. We should probably call it out in the release notes, though.

It looks like there is some inconsistency between how Spring Boot resolves properties and Spring Framework

There are many differences as Framework does not have the concept of relaxed binding. I don't think that's a strong enough reason to sacrifice the performance improvements or to have the implementation deliberately deviate from the documentation.

Comment From: nosan

Before version 3.5.0, Spring Boot's Relaxed Binding was a kind of superset of what the Spring Framework could do. It was mostly one-way: if the Spring Framework couldn't do something, there was a high chance that Spring Boot could, but not the other way around.

Since version 3.5.0, the situation has changed. Now, the Spring Framework can inject some properties via @Value (thanks to SystemEnvironmentPropertySource), but Spring Boot can't, which is quite unusual. Spring Boot is built on top of the Spring Framework, so it is expected that Spring Boot supports all the features of the Spring Framework along with its own additions.

However, this is only a partial issue. The bigger issue, as I see it, is that SystemEnvironmentPropertySource is still being used, for instance, in @Conditional.

The following test highlights the considerable difficulties developers might encounter when debugging their applications in such scenarios. (Spring Boot 3.5.0)


import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.env.SystemEnvironmentPropertySource;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Map;

class RelaxBindingTests {

    @Test
    void relaxBinding() {
        ConfigurableEnvironment environment = new StandardEnvironment();
        SystemEnvironmentPropertySource propertySource = new SystemEnvironmentPropertySource(
                StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
                Map.of("spring_main_cloud_platform", "kubernetes",
                        "test_value_name", "true"));
        environment.getPropertySources()
                .replace(propertySource.getName(), propertySource);
        try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestApplication.class)
                .web(WebApplicationType.NONE)
                .environment(environment)
                .run()) {
            assertThat(context.getBeanNamesForType(KubernetesService.class)).hasSize(1);
            assertThat(context.getBeanNamesForType(CustomBean.class)).hasSize(1);
            environment = context.getEnvironment();
            Binder binder = Binder.get(environment);
            assertThat(binder.bind("test.value-name", String.class).orElse(null)).isEqualTo("true");
            assertThat(binder.bind("spring.main.cloud-platform", String.class).orElse(null)).isEqualTo("kubernetes");
            assertThat(context.getBean(TestProperties.class).getValueName()).isEqualTo("true");
        }
    }


    @ConfigurationProperties("test")
    static class TestProperties {

        private String valueName;

        public String getValueName() {
            return valueName;
        }

        public void setValueName(String valueName) {
            this.valueName = valueName;
        }
    }

    @ConditionalOnBooleanProperty(name = "test.value-name")
    static class CustomBean {

    }

    @Configuration(proxyBeanMethods = false)
    @Import({KubernetesService.class, CustomBean.class})
    @EnableConfigurationProperties(TestProperties.class)
    static class TestApplication {

    }

    @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
    static class KubernetesService {

        @Override
        public String toString() {
            return "Hello Kubernetes!";
        }
    }
}


Both @Conditional annotations worked fine without any issues; however, I am unable to bind these properties.

Comment From: wilkinsona

Thanks, @nosan. I'd overlooked the fact that things like @ConditionalOnProperty calling the environment directly would result in different behavior across Spring Boot features. I think it's important that they align so we need to do something here.

Comment From: philwebb

I think the main problem is that we no longer support system environment properties that are not uppercase. I've pushed something I hope will fix that.