Having the following properties class:
@ConfigurationProperties(prefix = "app")
public class Configuration {
private Map<String, Object> properties;
public Map<String, Object> getProperties() {
return properties;
}
public void setProperties(Map<String, Object> properties) {
this.properties = properties;
}
}
And the following application.yml configuration:
spring:
application:
name: propertysourceissue
app:
properties:
name: propertysourceissue
version: 1.0.0
list:
- name: hi
- name: hello
(Note the app.properties.list property is a yml list)
The configuration.properties field looks as follows (json format for better understanding):
{
"app": {
"properties": {
"name": "propertysourceissue",
"version": "1.0.0",
"list": {
"0": { "name": "hi" },
"1": { "name": "hello" }
}
}
}
}
Note the app.properties.list property is not a list, but a map, where the index is the key. I think there is an issue in the conversion and the right structure should be as follows:
{
"app": {
"properties": {
"name": "propertysourceissue",
"version": "1.0.0",
"list": [
{ "name": "hi" },
{ "name": "hello" }
]
}
}
}
This is a Spring Boot project showing the issue, there are two tests there: one showing the expected behavior, one showing the current behavior.
Any clue on how to workaround this issue? Thanks in advance
Comment From: philwebb
This is working as expected since Map<String, Object> properties contains object values. Ideally Object would be a richer type that the binder could understand.
For a bit more background, there are a number of steps involved getting the YAML into a bound object. The Binder doesn't actually know about YAML.
The first set is the application.yaml is flattened and added to the Environment as a PropertySource. Given the following YAML:
app:
properties:
name: propertysourceissue
version: 1.0.0
list:
- name: hi
- name: hello
The actual content added to the PropertySource is:
app.properties.name=propertysourceissue
app.properties.version=1.0.0
app.properties.list[0].name=hi
app.properties.list[1].name=hello
The Binder then attempts to bind app.properties to a Map<String,Object>. It will add name, version and list keys. The values for name and version are added as you expect. For the list key it sees that there are nested parts to the name and since it's binding to an Object it will try to create a Map value to bind. To do this is recurses and added 0 and 1 as keys. Each of those recurses again and you get a second nested Map with name in the key.
In order to bind complex types, those types must in your Java code. For example, if you have:
@ConfigurationProperties(prefix = "app")
public class Configuration {
private Properties properties;
public Properties getProperties() {
return properties;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
record Properties(String name, String version, List<Item> list) {
}
record Item(String name) {
}
}
Then I think things will bind.
Comment From: philwebb
See also https://github.com/spring-projects/spring-boot/issues/22393#issuecomment-660823118