5

How can one map an application.yaml configuration with nested properties to a similar record structure in Java?

E.g., if we have the following yaml:

foo:
    bar:
        something: 42

    baz:
        otherThing: true

    color: blue

The desired record structure would be something along the lines of:

@ConfigurationProperties(prefix = "foo")
@ConstructorBinding
public record Foo(
    Bar bar,
    Baz baz,
    String color
) {}

// ---

@ConfigurationProperties(prefix = "foo.bar")
@ConstructorBinding
public record Bar(
    int something
) {}

// ---

@ConfigurationProperties(prefix = "foo.baz")
@ConstructorBinding
public record Baz(
    boolean otherThing
) {}
Naman
  • 27,789
  • 26
  • 218
  • 353
Rohde Fischer
  • 1,248
  • 2
  • 10
  • 32
  • 1
    On the first glance it looks like it should work. Have you tried removing `@ConfigurationProperties` from records `Baz` and `Bar` because it will be pulled by the property name in `Foo`? Or, nest records `Baz` and `Bar` inside `Foo` and remove `@ConfigurationProperties` from nested records if that is acceptable solution for you. – Boris Jan 12 '22 at 15:08
  • Turns out it does, I didn't reconstruct my issue correct enough it turns out. I finally managed to figure out what was wrong from the fact that it works and then diving step by step. Will post an answer to the question I intended to ask had I known what to ask – Rohde Fischer Jan 17 '22 at 12:11
  • Regarding the removing of `@ConfigurationProperties` that only works if I don't inject `Bar` and or `Baz` without `Foo`. I want to be able to inject only what I need to limit coupling and dependencies, so I'd actually not recommend removing them :) – Rohde Fischer Jan 17 '22 at 12:24

5 Answers5

3

I think for simplification you can just create a single file with:

@ConfigurationProperties(prefix = "foo")
public record Foo(Bar bar) {
    public record Bar(Baz baz) {
        public record Baz(String bum) {}    
    }
}

and this is working fine in spring-boot and you don't need to repeat annotations and when using it you will just use:

String bumVal = foo.bar().baz().bum();

where foo is just injected in your Bean(s) where you need it.

I removed even the @ConsructorBinding as since spring-boot 2.6 it is no more needed as long the record defines only one constructor, see release notes.

The above configuration relates to your own answer structure, but here is also the compact way for the original questions structure:

@ConfigurationProperties(prefix = "foo")
public record Foo(Bar bar, Baz baz, String color) {
    public record Bar(String something) {
    }
    public record Baz(String otherThing) {
    }   
}

I found the recordtype very useful for this use case as very compact and not too much code to write.

рüффп
  • 5,172
  • 34
  • 67
  • 113
  • Hi, this gives me `No qualifying bean of type Baz available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}` with Spring Boot 3.0. Do you know why? – apcuk Mar 28 '23 at 23:57
  • I wrote this answer and still using spring-boot 2.7.x. Are you sure you have the exact code I posted? – рüффп Mar 29 '23 at 20:19
  • how do you inject your properties? in my code you have to inject the Foo object which contains the Bar and Baz objects, where you can retreive them by calling e.g. (last example: foo.baz()) -> I think you cannot directly only inject the middle object of the config. – рüффп Mar 29 '23 at 20:59
  • 1
    Hi! Thanks for the reply. The issue was elsewhere, in my test configuration (it behaves a bit weird with SpyBean etc.), it is indeed working. – apcuk Mar 30 '23 at 16:07
  • 1
    This is how I am going to make all configuration properties from now on! – GreenSaguaro May 05 '23 at 02:27
2

You don't need @ConfigurationProperties for each nested class. It only for the root class (Foo.class). Then make the Foo as Spring Bean by inserting @Component above the class or put @ConfigurationPropertiesScan on the Application class.

Ferry
  • 344
  • 3
  • 10
  • Thanks, turns out I didn't ask the question I intended to. Accepting your answer, since you answered what I actually asked, and will add the answer below that I intended in case others runs into a similar issue :) – Rohde Fischer Jan 17 '22 at 12:13
  • Regarding the removing of `@ConfigurationProperties` that only works if I don't inject `Bar` and or `Baz` without `Foo`. I want to be able to inject only what I need to limit coupling and dependencies, so I'd actually not recommend removing them :) – Rohde Fischer Jan 17 '22 at 12:24
1

Turns out I didn't ask the correct question for the issue I had :/ So for the case people find this topic from a similar issue, the answer to my actual issue follows here.

The problem arises with a nested yaml trying to "short cut" on the model hierarchy, so given the following yaml:

foo:
    bar:
        baz:
            bum: "hello"

I was trying to model the hierarchy as follows:

@ConfigurationProperties(prefix = "foo")
@ConstructorBinding
public record Foo(BarBaz barBaz) {}

// --- 

@ConfigurationProperties(prefix = "foo.bar.baz")
@ConstructorBinding
public record BarBaz(String bum) {}

Here the problem arises that Foo cannot do constructor binding for BarBaz (not sure why). So there are two possible solutions that I found:

1. Do the full modelling (decided that this is what I prefer)

That is, don't try to skip the middle model for bar.

@ConfigurationProperties(prefix = "foo")
@ConstructorBinding
public record Foo(Bar bar) {}

// ---

@ConfigurationProperties(prefix = "foo.bar")
@ConstructorBinding
public record Bar(Baz baz) {}

// --- 

@ConfigurationProperties(prefix = "foo.bar.baz")
@ConstructorBinding
public record Baz(String bum) {}

2. Don't use @ConstructorBinding when embedding more nestings

Simply skip the constructor binding in Foo.

@ConfigurationProperties(prefix = "foo")
public record Foo(BarBaz barBaz) {}

// --- 

@ConfigurationProperties(prefix = "foo.bar.baz")
@ConstructorBinding
public record BarBaz(String bum) {}

Although simpler, it's less consistent.

Rohde Fischer
  • 1,248
  • 2
  • 10
  • 32
0

I've had an oddly difficult time finding succinct, practical, working examples of this, so I thought I'd share a bit of code from after I pieced it together and got it working.

For some application.yml like this:

spring:
    security:
        oauth2:
            authorizationserver:
                client:
                    oidc-client:
                        registration:
                            client-id: "client ID value"
                            client-authentication-methods:
                                - "value 1"
                                - "value 2"
                                - "atc."

I created a class like this:

@Configuration
@ConfigurationPropertiesScan 
@ConfigurationProperties(prefix = "spring.security.oauth2.authorizationserver.client.oidc-client.registration")
public class RewindOidcConfiguration {

    private String clientId;
    public void setClientId(String value) { this.clientId = value; }

    private List<String> clientAuthenticationMethods;
    public void setClientAuthenticationMethods(List<String> value) { this.clientAuthenticationMethods = value; }

    ...

}

This sets the private fields which are then available for you to use as you wish. This demonstrates hierarchy, flexible binding for the names, list values, the need for setters (which Spring calls), etc.

Todd
  • 99
  • 1
  • 4
0

One point I noted is that if we use the lombok annotation @AllArgsConstructor on the bean it won't work. Just use only @Data which gives setters and getters and a few others. Seems this piece of configuration works based off of setter injections. I spent lot of time figuring this out.