Mapping configuration to objects
With config mappings it is possible to group multiple configuration properties in a single interface that share the same prefix.
1. @ConfigMapping
A config mapping requires a public interface with minimal metadata configuration and annotated with the
@io.smallrye.config.ConfigMapping
annotation.
@ConfigMapping(prefix = "server")
public interface Server {
String host();
int port();
}
The Server
interface is able to map configuration properties with the name server.host
into the Server.host()
method and server.port
into Server.port()
method. The configuration property name to look up is built from the
prefix, and the method name with .
(dot) as the separator.
If a mapping fails to match a configuration property a NoSuchElementException is thrown, unless the mapped
element is an Optional .
|
1.1. Registration
When a Quarkus application starts, a config mapping can be registered twice. One time for STATIC INIT and a second time for RUNTIME INIT:
1.1.1. STATIC INIT
Quarkus starts some of its services during static initialization, and Config
is usually one of the first things that
is created. In certain situations it may not be possible to correctly initialize a config mapping. For instance, if the
mapping requires values from a custom ConfigSource
. For this reason, any config mapping requires the annotation
@io.quarkus.runtime.configuration.StaticInitSafe
to mark the mapping as safe to be used at this stage. Learn more
about registration of a custom ConfigSource
.
1.2. Retrieval
A config mapping interface can be injected into any CDI aware bean:
class BusinessBean {
@Inject
Server server;
public void businessMethod() {
String host = server.host();
}
}
In non-CDI contexts, use the API io.smallrye.config.SmallRyeConfig#getConfigMapping
to retrieve the config mapping
instance:
SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class);
Server server = config.getConfigMapping(Server.class);
1.3. Nested groups
A nested mapping provides a way to subgroup other config properties:
@ConfigMapping(prefix = "server")
public interface Server {
String host();
int port();
Log log();
interface Log {
boolean enabled();
String suffix();
boolean rotate();
}
}
server.host=localhost
server.port=8080
server.log.enabled=true
server.log.suffix=.log
server.log.rotate=false
The method name of a mapping group acts as sub-namespace to the configurations properties.
1.4. Overriding property names
1.4.1. @WithName
If a method name, or a property name do not match with each other, the @WithName
annotation can override the method
name mapping and use the name supplied in the annotation:
@ConfigMapping(prefix = "server")
public interface Server {
@WithName("name")
String host();
int port();
}
server.name=localhost
server.port=8080
1.4.2. @WithParentName
The @WithParentName
annotation allows to configurations mapping to inherit its container name, simplifying the
configuration property name required to match the mapping:
interface Server {
@WithParentName
ServerHostAndPort hostAndPort();
@WithParentName
ServerInfo info();
}
interface ServerHostAndPort {
String host();
int port();
}
interface ServerInfo {
String name();
}
server.host=localhost
server.port=8080
server.name=konoha
Without the @WithParentName
the method name()
requires the configuration property server.info.name
. Because we use
@WithParentName
, the info()
mapping will inherit the parent name from Server
and name()
maps to server.name
instead.
1.4.3. NamingStrategy
Method names in camelCase map to kebab-case property names:
@ConfigMapping(prefix = "server")
public interface Server {
String theHost();
int thePort();
}
server.the-host=localhost
server.the-port=8080
The mapping strategy can be adjusted by setting namingStrategy
value in the @ConfigMapping
annotation:
@ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM)
public interface ServerVerbatimNamingStrategy {
String theHost();
int thePort();
}
server.theHost=localhost
server.thePort=8080
The @ConfigMapping
annotation support the following naming strategies:
-
KEBAB_CASE
(default) - The method name is derived by replacing case changes with a dash to map the configuration property. -
VERBATIM
- The method name is used as is to map the configuration property. -
SNAKE_CASE
- The method name is derived by replacing case changes with an underscore to map the configuration property.
1.5. Conversions
A config mapping class support automatic conversions of all types available for conversion in Config
:
@ConfigMapping
public interface SomeTypes {
@WithName("int")
int intPrimitive();
@WithName("int")
Integer intWrapper();
@WithName("long")
long longPrimitive();
@WithName("long")
Long longWrapper();
@WithName("float")
float floatPrimitive();
@WithName("float")
Float floatWrapper();
@WithName("double")
double doublePrimitive();
@WithName("double")
Double doubleWrapper();
@WithName("char")
char charPrimitive();
@WithName("char")
Character charWrapper();
@WithName("boolean")
boolean booleanPrimitive();
@WithName("boolean")
Boolean booleanWrapper();
}
int=9
long=9999999999
float=99.9
double=99.99
char=c
boolean=true
This is also valid for Optional
and friends:
@ConfigMapping
public interface Optionals {
Optional<Server> server();
Optional<String> optional();
@WithName("optional.int")
OptionalInt optionalInt();
interface Server {
String host();
int port();
}
}
In this case, the mapping won’t fail if there is no configuration property to match the mapping.
1.5.1. @WithConverter
The @WithConverter
annotation provides a way to set a Converter
to use in a specific mapping:
@ConfigMapping
public interface Converters {
@WithConverter(FooBarConverter.class)
String foo();
}
public static class FooBarConverter implements Converter<String> {
@Override
public String convert(final String value) {
return "bar";
}
}
foo=foo
A call to Converters.foo()
results in the value bar
.
1.5.2. Collections
A config mapping is also able to map collections types List
and Set
:
@ConfigMapping(prefix = "server")
public interface ServerCollections {
Set<Environment> environments();
interface Environment {
String name();
List<App> apps();
interface App {
String name();
List<String> services();
Optional<List<String>> databases();
}
}
}
server.environments[0].name=dev
server.environments[0].apps[0].name=rest
server.environments[0].apps[0].services=bookstore,registration
server.environments[0].apps[0].databases=pg,h2
server.environments[0].apps[1].name=batch
server.environments[0].apps[1].services=stock,warehouse
The List
or Set
mappings can use indexed properties to map
configuration values in mapping groups. For collection with simple element types like String
, their configuration
value is a comma separated string.
Only the List mapping can maintain element order. Hence, with Set mappings the element order is not maintained from the configuration files but is random.
|
1.5.3. Maps
A config mapping is also able to map a Map
:
@ConfigMapping(prefix = "server")
public interface Server {
String host();
int port();
Map<String, String> form();
}
server.host=localhost
server.port=8080
server.form.login-page=login.html
server.form.error-page=error.html
server.form.landing-page=index.html
The configuration property needs to specify an additional name to act as the key. In this case the form()
Map
will
contain three elements with the keys login-page
, error-page
and landing-page
.
It also works for groups:
@ConfigMapping(prefix = "server")
public interface Servers {
@WithParentName
Map<String, Server> allServers();
}
public interface Server {
String host();
int port();
String login();
String error();
String landing();
}
server."my-server".host=localhost
server."my-server".port=8080
server."my-server".login=login.html
server."my-server".error=error.html
server."my-server".landing=index.html
In this case the allServers()
Map
will
contain one Server
element with the key my-server
.
1.6. Defaults
The @WithDefault
annotation allows to set a default property into a mapping (and prevent and error if the
configuration value is not available in any ConfigSource
):
public interface Defaults {
@WithDefault("foo")
String foo();
@WithDefault("bar")
String bar();
}
No configuration properties required. The Defaults.foo()
will return the value foo
and Defaults.bar()
will return
the value bar
.
1.7. Validation
A config mapping may combine annotations from Bean Validation to validate configuration values:
@ConfigMapping(prefix = "server")
public interface Server {
@Size(min = 2, max = 20)
String host();
@Max(10000)
int port();
}
For validation to work, the quarkus-hibernate-validator extension is required, and it is performed
automatically.
|
1.8. Mocking
A mapping interface implementation is not a proxy, so it cannot be mocked directly with @InjectMock
like other CDI
beans. One trick is to make it proxyable with a producer method:
public class ServerMockProducer {
@Inject
Config config;
@Produces
@ApplicationScoped
@io.quarkus.test.Mock
Server server() {
return config.unwrap(SmallRyeConfig.class).getConfigMapping(Server.class);
}
}
The Server
can be injected as a mock into a Quarkus test class with @InjectMock
:
@QuarkusTest
class ServerMockTest {
@InjectMock
Server server;
@Test
void localhost() {
Mockito.when(server.host()).thenReturn("localhost");
assertEquals("localhost", server.host());
}
}
The mock is just an empty shell without any actual configuration values. |
If the goal is to only mock certain configuration values and retain the original configuration, the mocking instance requires a spy:
@ConfigMapping(prefix = "app")
public interface AppConfig {
@WithDefault("app")
String name();
Info info();
interface Info {
@WithDefault("alias")
String alias();
@WithDefault("10")
Integer count();
}
}
public static class AppConfigProducer {
@Inject
Config config;
@Produces
@ApplicationScoped
@io.quarkus.test.Mock
AppConfig appConfig() {
AppConfig appConfig = config.unwrap(SmallRyeConfig.class).getConfigMapping(AppConfig.class);
AppConfig appConfigSpy = Mockito.spy(appConfig);
AppConfig.Info infoSpy = Mockito.spy(appConfig.info());
Mockito.when(appConfigSpy.info()).thenReturn(infoSpy);
return appConfigSpy;
}
}
The AppConfig
can be injected as a mock into a Quarkus test class with @Inject
:
@QuarkusTest
class AppConfigTest {
@Inject
AppConfig appConfig;
@Test
void localhost() {
Mockito.when(appConfig.name()).thenReturn("mocked-app");
assertEquals("mocked-app", server.host());
Mockito.when(appConfig.info().alias()).thenReturn("mocked-alias");
assertEquals("mocked-alias", server.info().alias());
}
}
Nested elements need to be spied individually by Mockito. |