Spring boot version: 2.2.7.RELEASE

Consider below test case:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

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

@SpringBootTest
@EnableConfigurationProperties({BindingTest.FooProperties.class, BindingTest.BarProperties.class})
@TestPropertySource(properties = {"foo.bar=baz"})
public class BindingTest {

    @Autowired
    FooProperties fooProperties;

    @Test
    public void shouldInjectBar() {
        assertThat(fooProperties.barProperties.getBar()).isEqualTo("baz");
    }

    @ConstructorBinding
    @ConfigurationProperties("foo")
    public static class FooProperties {

        private final BarProperties barProperties;

        @Autowired
        public FooProperties(BarProperties barProperties) {
            this.barProperties = barProperties;
        }

        public BarProperties getBarProperties() {
            return barProperties;
        }

    }

    @ConstructorBinding
    @ConfigurationProperties("foo.bar")
    public static class BarProperties {

        private final String bar;

        public BarProperties(String bar) {
            this.bar = bar;
        }

        public String getBar() {
            return bar;
        }

    }

}

Constructor injection of BarProperties is expected to be done either automatically by spring boot or by explicit use of @Autowired annotation in the constructor but BarProperties is Null.

If we add @Autowired to the field then the injection happens and the property is resolved.

    @ConstructorBinding
    @ConfigurationProperties("foo")
    public static class FooProperties {

        @Autowired
        private final BarProperties barProperties;

        public FooProperties(BarProperties barProperties) {
            this.barProperties = barProperties;
        }

        public BarProperties getBarProperties() {
            return barProperties;
        }

    }

Comment From: wilkinsona

Thanks for the sample.

When you nest properties, as you have done with BarProperties nested beneath FooProperties, you don't need @ConfigurationProperties on the nested type. It's the prefix of FooProperties prefix and the name of the getter that determines the prefix of the properties in BarProperties. In this case it will be foo.barProperties and the property to set is foo.barProperties.bar. You haven't set that property so the behaviour that is described in the documentation becomes relevant:

By default, if no properties are bound to Security, the AcmeProperties instance will contain a null value for security. If you wish you return a non-null instance of Security even when no properties are bound to it, you can use an empty @DefaultValue annotation to do so.

In your case, FooProperties is the equivalent of AcmeProperties and BarProperties is the equivalent of Security. Adding @DefaultProperties to the BarProperties argument of FooProperties' constructor will cause an instance of BarProperties to be injected but bar will be null as no value has been bound to it. To bind a value to it, the foo.barProperties.bar property must be set. Your test will pass if you make the following change to its TestPropertySource:

@TestPropertySource(properties = {"foo.barProperties.bar=baz"})

Updating your test to align with the structure described in the documentation that I linked to above, it then looks like the following:

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

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

@SpringBootTest
@EnableConfigurationProperties(BindingTest.FooProperties.class)
@TestPropertySource(properties = {"foo.bar.baz=baz"})
public class BindingTest {

    @Autowired
    FooProperties fooProperties;

    @Test
    public void shouldInjectBar() {
        assertThat(fooProperties.bar.getBaz()).isEqualTo("baz");
    }

    @ConstructorBinding
    @ConfigurationProperties("foo")
    public static class FooProperties {

        private final Bar bar;

        @Autowired
        public FooProperties(@DefaultValue Bar bar) {
            this.bar = bar;
        }

        public Bar getBarProperties() {
            return bar;
        }

        static class Bar {

            private final String baz;

            public Bar(String baz) {
                this.baz = baz;
            }

            public String getBaz() {
                return baz;
            }

        }

    }

}

If you have any further questions, please follow up on Stack Overflow or Gitter. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.

Comment From: HarshadRanganathan

@wilkinsona Probably, I should have added some details around what I'm trying to achieve along with the failing test case. I understand that nested properties would work in the way you have shared in your test case.

My use case is what if I need the nested property to be defined in it's own class file rather than as a static inner class to avoid clutter.

Let's say I have two properties foo.hello=world,foo.bar.baz.bar.baz=baz which would result in a deeply nested structure for binding property foo.bar.baz.bar.baz.

I could simply avoid it by defining a class with @ConfigurationProperties("foo.bar.baz.bar") and autowire it in the other class having @ConfigurationProperties("foo").

That's the example I had shown in my test case and the question was why autowiring works at field level but not at a constructor level.

Hope I had clarified the intention.

Comment From: wilkinsona

As noted in the documentation, injecting dependencies into @ConfigurationProperties classes is discouraged and isn't supported at all with @ConstructorBinding:

We recommend that @ConfigurationProperties only deal with the environment and, in particular, does not inject other beans from the context. For corner cases, setter injection can be used or any of the *Aware interfaces provided by the framework (such as EnvironmentAware if you need access to the Environment). If you still want to inject other beans using the constructor, the configuration properties bean must be annotated with @Component and use JavaBean-based property binding.

As I said above, if you have any further questions please follow up on Stack Overflow or Gitter.