Spring Map Binding: Something you need to know
2025-03-01
Recently, I came across an interesting behavior in Spring - one that perfectly captures its ✨magic✨ in action. So, I thought it's worth sharing as a fun fact and something to know about.
Real-Life Scenario
As experienced developers, we follow the principles of The Twelve-Factor App and know that configuration should be kept separate from the application code.
Now, let’s say we’re building a multi-tenant application. At some point, we might need a Map to associate certain settings — like time zones — with each tenant. A simplified version of this might look something like this:
package com.tulski.relaxedproperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import java.util.Map;
@SpringBootApplication
@EnableConfigurationProperties(RelaxedPropertiesApplication.AppConfig.class)
public class RelaxedPropertiesApplication {
private static final Logger log = LoggerFactory.getLogger(RelaxedPropertiesApplication.class);
public static void main(String[] args) {
final var context = SpringApplication.run(RelaxedPropertiesApplication.class, args);
final var appConfig = context.getBean(AppConfig.class);
log.info("Output config:\n{}", appConfig);
}
@ConfigurationProperties(prefix = "app-config")
record AppConfig(Map<String, String> tenantTimezone) {
}
}
Config Data Files
The most common way to set values is to use config data files (such as application.properties or application.yaml).
# YAML
app-config:
tenant-timezone:
PL: Europe/Warsaw
DE: Europe/Berlin
# Properties
app-config.tenant-timezone.PL=Europe/Warsaw
app-config.tenant-timezone.DE=Europe/Berlin
Both solutions are very intuitive, so the outcome of running the RelaxedPropertiesApplication
should not surprise anyone.
# output
AppConfig[
tenantTimezone={
PL=Europe/Warsaw
DE=Europe/Berlin
}
]
Environment Variables
Another available solution is to use environment variables.
If we use envs, we need to be aware that map keys will be in lower case.
Why? When Spring Boot processes system environment properties, it converts them from SCREAMING_SNAKE_CASE
into properties notation (lowercase and dotted).
Spring mentions this in its documentation.
Only the environment variable name is lower-cased, not the value. When setting
MY_PROPS_VALUES_KEY=VALUE
, the values Map contains a{"key"="VALUE"}
entry.
source: Binding Maps From Environment Variables
# envs
APPCONFIG_TENANTTIMEZONE_PL=Europe/Warsaw
APPCONFIG_TENANTTIMEZONE_DE=Europe/Berlin
# properties notation
appconfig.tenanttimezone.pl=Europe/Warsaw
appconfig.tenanttimezone.de=Europe/Berlin
# output
AppConfig[
tenantTimezone={
pl=Europe/Warsaw,
de=Europe/Berlin
}
]
Binding Maps From YAML File and Environment Variables
Ok, what if I configure the application both in a configuration file and via environment variables at the same time? Environment variables will be converted to properties notation and, using relaxed binding, will override the config data file. OS environment variables take precedence over configuration data files.
# output
AppConfig[
tenantTimezone={
pl=Europe/Warsaw,
de=Europe/Berlin,
PL=Europe/Warsaw,
DE=Europe/Berlin
}
]
Notice the pairs of pl
and PL
and DE
and de
keys - they are sort of "duplicated".
But what does it really mean for variables to be applied using relaxed binding? Let’s take a look at this example.
# application.yaml
app-config:
tenant-timezone:
EN: America/New_York
En: America/New_York
eN: America/New_York
E-N: America/New_York
e-n: America/New_York
# envs
APPCONFIG_TENANTTIMEZONE_EN=Europe/London
# outcome
AppConfig[
tenantTimezone={
en=Europe/London,
EN=Europe/London,
En=Europe/London,
eN=Europe/London,
E-N=Europe/London,
e-n=Europe/London
}
]
APPCONFIG_TENANTTIMEZONE_EN
was matched to each key in the config data file and overwrote its value.
Summary
-
Keys from environment variable are always lowercase: Spring Boot automatically converts environment variable keys to lowercase when applying them.
-
Be careful when binding values from environment variables with default values set in YAML - it can end up with ‘duplicate’ keys.
-
Consider normalizing keys in your code: To avoid potential mismatches or confusion, it's a good practice to standardize key formats in your application code.
Key normalization
The code with key normalization might look like the one below:
@@ -8,6 +8,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import java.util.Map;
+import java.util.stream.Collectors;
@SpringBootApplication
@EnableConfigurationProperties(RelaxedPropertiesApplication.AppConfig.class)
@@ -23,6 +24,14 @@ public class RelaxedPropertiesApplication {
@ConfigurationProperties(prefix = "app-config")
record AppConfig(Map<String, String> tenantTimezone) {
+
+ AppConfig(Map<String, String> tenantTimezone) {
+ this.tenantTimezone = tenantTimezone.entrySet().stream()
+ .collect(Collectors.toMap(
+ entry -> entry.getKey().toUpperCase(),
+ Map.Entry::getValue
+ ));
+ }
}
}
The source code can be found on my Github.