Reputation: 1482
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
) {}
Upvotes: 10
Views: 14584
Reputation: 1
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.
Upvotes: 0
Reputation: 642
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.
Upvotes: 0
Reputation: 5448
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 record
type very useful for this use case as very compact and not too much code to write.
Upvotes: 10
Reputation: 1482
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.
Upvotes: 1
Reputation: 414
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.
Upvotes: 5