Hello,

I have found interesting behavior when properties are binded from environment variables to map with list as a value. For first case map is called "second", in this case "first-key" property is duplicated two times as "first.key" and "first-key" For second case map is called "secondMap", in this case "first-key" is not duplicated.


First case:

public class Something {

    private String field1;
    private String field2;

    public String getField1() {
        return field1;
    }

    public void setField1(String field1) {
        this.field1 = field1;
    }

    public String getField2() {
        return field2;
    }

    public void setField2(String field2) {
        this.field2 = field2;
    }

}
@Configuration
@ConfigurationProperties("config.test")
public class Config {

    private Map<String, Something> first;
    private Map<String, List<Something>> second;

    public Map<String, Something> getFirst() {
        return first;
    }

    public void setFirst(Map<String, Something> first) {
        this.first = first;
    }

    public Map<String, List<Something>> getSecond() {
        return second;
    }

    public void setSecond(Map<String, List<Something>> second) {
        this.second = second;
    }

}

application.yml:

config:
  test:
    first:
      first-key:
        field1: ${CONFIG_TEST_FIRST_FIRST_KEY_FIELD1}
        field2: ${CONFIG_TEST_FIRST_FIRST_KEY_FIELD2}
      second-key:
        field1: ${CONFIG_TEST_FIRST_SECOND_KEY_FIELD1}
        field2: ${CONFIG_TEST_FIRST_SECOND_KEY_FIELD2}

    second:
      first-key:
        - field1: ${CONFIG_TEST_SECOND_FIRST_KEY_0_FIELD1}
          field2: ${CONFIG_TEST_SECOND_FIRST_KEY_0_FIELD2}
        - field1: ${CONFIG_TEST_SECOND_FIRST_KEY_1_FIELD1}
          field2: ${CONFIG_TEST_SECOND_FIRST_KEY_1_FIELD2}

Enviroments:

CONFIG_TEST_FIRST_FIRST_KEY_FIELD1=field1
CONFIG_TEST_FIRST_FIRST_KEY_FIELD2=field2

CONFIG_TEST_FIRST_SECOND_KEY_FIELD1=field1
CONFIG_TEST_FIRST_SECOND_KEY_FIELD2=field2

CONFIG_TEST_SECOND_FIRST_KEY_0_FIELD1=field1
CONFIG_TEST_SECOND_FIRST_KEY_0_FIELD2=field2

CONFIG_TEST_SECOND_FIRST_KEY_1_FIELD1=field1
CONFIG_TEST_SECOND_FIRST_KEY_1_FIELD2=field2

Output:

{
  {
    first-key=Something [field1=field1, field2=field2], second-key=Something [field1=field1, field2=field2]
  }
  {
    first.key=[Something [field1=field1, field2=field2], Something [field1=field1, field2=field2]],
    first-key=[Something [field1=field1, field2=field2], Something [field1=field1, field2=field2]]
  }
}

Second case:

@Configuration
@ConfigurationProperties("config.test")
public class Config {

    private Map<String, Something> firstMap;
    private Map<String, List<Something>> secondMap;

    public Map<String, Something> getFirstMap() {
        return firstMap;
    }

    public void setFirstMap(Map<String, Something> firstMap) {
        this.firstMap = firstMap;
    }

    public Map<String, List<Something>> getSecondMap() {
        return secondMap;
    }

    public void setSecondMap(Map<String, List<Something>> secondMap) {
        this.secondMap = secondMap;
    }

}

application.yml:

config:
  test:
    first-map:
      first-key:
        field1: ${CONFIG_TEST_FIRST_MAP_FIRST_KEY_FIELD1}
        field2: ${CONFIG_TEST_FIRST_MAP_FIRST_KEY_FIELD2}
      second-key:
        field1: ${CONFIG_TEST_FIRST_MAP_SECOND_KEY_FIELD1}
        field2: ${CONFIG_TEST_FIRST_MAP_SECOND_KEY_FIELD2}

    second-map:
      first-key:
        - field1: ${CONFIG_TEST_SECOND_MAP_FIRST_KEY_0_FIELD1}
          field2: ${CONFIG_TEST_SECOND_MAP_FIRST_KEY_0_FIELD2}
        - field1: ${CONFIG_TEST_SECOND_MAP_FIRST_KEY_1_FIELD1}
          field2: ${CONFIG_TEST_SECOND_MAP_FIRST_KEY_1_FIELD2}

Enviroments:

CONFIG_TEST_FIRST_MAP_FIRST_KEY_FIELD1=field1
CONFIG_TEST_FIRST_MAP_FIRST_KEY_FIELD2=field2

CONFIG_TEST_FIRST_MAP_SECOND_KEY_FIELD1=field1
CONFIG_TEST_FIRST_MAP_SECOND_KEY_FIELD2=field2

CONFIG_TEST_SECOND_MAP_FIRST_KEY_0_FIELD1=field1
CONFIG_TEST_SECOND_MAP_FIRST_KEY_0_FIELD2=field2

CONFIG_TEST_SECOND_MAP_FIRST_KEY_1_FIELD1=field1
CONFIG_TEST_SECOND_MAP_FIRST_KEY_1_FIELD2=field2

Output:

{
  {
    first-key=Something [field1=field1, field2=field2], second-key=Something [field1=field1, field2=field2]
  }
  {
    first-key=[Something [field1=field1, field2=field2], Something [field1=field1, field2=field2]]
  }
}

Comment From: scottfrederick

@ivan909020 Thanks for getting in touch. It's difficult to tell exactly what's going on from the code snippets you've provided. The Something class you show isn't used by either of the @ConfigurationProperties classes and the @ConfigurationProperties classes have fields whose types are raw Map.

If you would like us to spend some time investigating, please provide a complete minimal sample that reproduces the problem so we can run it ourselves. You can share it with us by pushing it to a separate repository on GitHub or by zipping it and attaching it to this issue.

Comment From: philwebb

The initial text was using <pre> blocks so the generics were not visible. I've edited it to improve the formatting. You might want to check out this Mastering Markdown guide for future reference.

Comment From: ivan-zaitsev

I have used <pre> to highlight differences between two cases using bold text. Indeed, the generics are not visible for some browsers.

Comment From: ivan-zaitsev

@scottfrederick

case1.zip case2.zip

Comment From: scottfrederick

@ivan909020 This is working as designed. It looks a little strange in your examples due to the fact that you are using environment variables that map directly to your configuration properties class and also using the environment variables to map to properties in application.yml.

Environment variables are one of the many sources Spring Boot can use to populate properties. When inspecting the environment, Spring Boot will convert variables like CONFIG_TEST_FIRST_FIRST_KEY_FIELD1=field1 to a lower-case dotted notation (the inverse of what's described in the documentation) and then attempt to map them to @ConfigurationProperties beans.

In your example, these environment variables:

CONFIG_TEST_SECOND_FIRST_KEY_0_FIELD1=field1
CONFIG_TEST_SECOND_FIRST_KEY_0_FIELD2=field2

CONFIG_TEST_SECOND_FIRST_KEY_1_FIELD1=field1
CONFIG_TEST_SECOND_FIRST_KEY_1_FIELD2=field2

Will be convered into:

config.test.second.first.key.0.field1=field1
config.test.second.first.key.0.field2=field2

config.test.second.first.key.1.field1=field1
config.test.second.first.key.1.field2=field2

Since these keys start with config.test, they will be mapped into a bean annotated with @ConfigurationProperties("config.test"). To visualize how the environment variables get mapped to the bean, you can parse them into a hierarchy that would look like this:

config
  test
    second
      first.key
        0
          field1=field1
          field2=field2
        1
          field1=field1
          field2=field2

When these values are applied to the Config#setSecond(Map<String, List<Something>> second) method, you end up with the first.key=[Something [field1=field1, field2=field2], Something [field1=field1, field2=field2]] properties you are seeing.

This is separate and distinct from the properties that are read from the application.yml file:

config:
  test:
    second:
      first-key:
        - field1: ${CONFIG_TEST_SECOND_FIRST_KEY_0_FIELD1}
          field2: ${CONFIG_TEST_SECOND_FIRST_KEY_0_FIELD2}
        - field1: ${CONFIG_TEST_SECOND_FIRST_KEY_1_FIELD1}
          field2: ${CONFIG_TEST_SECOND_FIRST_KEY_1_FIELD2}

Which results in the properties 'first-key=[Something [field1=field1, field2=field2], Something [field1=field1, field2=field2]]'.

If your goal is to have the application.yml file be the source of all of these properties but have the values provided by environment variables, then you might want to use something other than CONFIG_TEST as a prefix for the environment variables.

Comment From: ivan-zaitsev

@scottfrederick Ok, then why in the second example with map name "secondMap" instead of "second" there are no duplicates? The difference between two examples just the map name.

Comment From: ivan-zaitsev

Thank you for detailed answer. I think the second case is without duplicates because environment variables will not be applied.

These environment variables:

CONFIG_TEST_SECOND_MAP_FIRST_KEY_0_FIELD1=field1
CONFIG_TEST_SECOND_MAP_FIRST_KEY_0_FIELD2=field2

CONFIG_TEST_SECOND_MAP_FIRST_KEY_1_FIELD1=field1
CONFIG_TEST_SECOND_MAP_FIRST_KEY_1_FIELD2=field2

Will be convered into:

config.test.second.map.first.key.0.field1=field1
config.test.second.map.first.key.0.field2=field2

config.test.second.map.first.key.1.field1=field1
config.test.second.map.first.key.1.field2=field2

To visualize how the environment variables get mapped to the bean, you can parse them into a hierarchy that would look like this:

config
  test
    second.map
      first.key
        0
          field1=field1
          field2=field2
        1
          field1=field1
          field2=field2

This is separate and distinct from the properties that are read from the application.yml file:

config:
  test:
    second-map:
      first-key:
        - field1: ${CONFIG_TEST_SECOND_MAP_FIRST_KEY_0_FIELD1}
          field2: ${CONFIG_TEST_SECOND_MAP_FIRST_KEY_0_FIELD2}
        - field1: ${CONFIG_TEST_SECOND_MAP_FIRST_KEY_1_FIELD1}
          field2: ${CONFIG_TEST_SECOND_MAP_FIRST_KEY_1_FIELD2}

"second.map" is different from "second-map" And in this case enviroment variables will not be applied because it will not find the map with name "second.map".