Mapping Configuration to Objects

Com os mapeamentos de configuração, é possível agrupar várias propriedades de configuração em uma única interface que compartilhe o mesmo prefixo.

1. @ConfigMapping

Um mapeamento de configuração requer uma interface pública com o mínimo de configuração de metadados e anotada com a anotação @io.smallrye.config.ConfigMapping.

@ConfigMapping(prefix = "server")
public interface Server {
    String host();

    int port();
}

A interface Server é capaz de mapear propriedades de configuração com o nome server.host para o método Server.host() e server.port para o método Server.port(). O nome da propriedade de configuração a ser consultada é construído a partir do prefixo e do nome do método, usando o . (ponto) como separador.

If a mapping fails to match a configuration property a NoSuchElementException is thrown, unless the mapped element is an Optional.

1.1. Mapping Rules

A config mapping interface must obey the following rules:

  • A mapping method cannot accept parameters

  • A mapping method return type cannot be void

  • A mapping cannot use self-reference types

  • default methods are allowed

1.2. Registro

Quando uma aplicação Quarkus inicia, um mapeamento de configuração pode ser registrado duas vezes. Uma vez para STATIC INIT e uma segunda vez para RUNTIME INIT :

1.2.1. STATIC INIT

O Quarkus inicia alguns de seus serviços durante a inicialização estática (static initialization), e o Config é geralmente uma das primeiras coisas a ser criada. Em certas situações, pode não ser possível inicializar corretamente um mapeamento de configuração. Por exemplo, se o mapeamento exigir valores de um ConfigSource personalizado. Por esse motivo, qualquer mapeamento de configuração requer a anotação @io.quarkus.runtime.configuration.StaticInitSafe para marcar o mapeamento como seguro para ser usado nesta etapa. Saiba mais sobre o registro de um ConfigSource personalizado.

1.2.1.1. Exemplo
@StaticInitSafe
@ConfigMapping(prefix = "server")
public interface Server {
    String host();

    int port();
}

1.2.2. RUNTIME INIT

O estágio RUNTIME INIT ocorre após o STATIC INIT. Não existem restrições nesse estágio, e qualquer mapeamento de configuração é adicionado à instância de Config conforme esperado.

1.3. Recuperação

Uma interface de mapeamento de configuração pode ser injetada em qualquer bean com reconhecimento de CDI:

class BusinessBean {
    @Inject
    Server server;

    public void businessMethod() {
        String host = server.host();
    }
}

Em contextos que não são CDI, utilize a API io.smallrye.config.SmallRyeConfig#getConfigMapping para recuperar a instância de mapeamento de configuração:

SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class);
Server server = config.getConfigMapping(Server.class);

1.4. Hierarchy

A config mapping can extend another mapping and inherit all its super members:

public interface Parent {
    String name();
}

@ConfigMapping(prefix = "child")
public interface Child extends Parent {

}

Members can also be overridden:

public interface Parent {
    String name();
}

@ConfigMapping(prefix = "child")
public interface Child extends Parent {
    @WithName("child-name")
    String name();
}

1.5. Grupos aninhados

Um mapeamento aninhado oferece uma maneira de subagrupar outras propriedades de configuração:

@ConfigMapping(prefix = "server")
public interface Server {
    String host();

    int port();

    Log log();

    interface Log {
        boolean enabled();

        String suffix();

        boolean rotate();
    }
}
application.properties
server.host=localhost
server.port=8080
server.log.enabled=true
server.log.suffix=.log
server.log.rotate=false

O nome do método de um grupo de mapeamento atua como sub-namespace para as propriedades de configuração.

1.6. Sobrescrevendo nomes de propriedades

1.6.1. @WithName

Se um nome de método ou um nome de propriedade não corresponderem um ao outro, a anotação @WithName pode sobrescrever o mapeamento do nome do método e utilizar o nome fornecido na anotação:

@ConfigMapping(prefix = "server")
public interface Server {
    @WithName("name")
    String host();

    int port();
}
application.properties
server.name=localhost
server.port=8080

1.6.2. @WithParentName

A anotação @WithParentName permite que a propriedade de mapeamento de configuração herde o nome do seu contêiner, simplificando o nome da propriedade de configuração necessário para corresponder ao mapeamento:

@ConfigMapping(prefix = "server")
interface Server {
    @WithParentName
    ServerHostAndPort hostAndPort();

    @WithParentName
    ServerInfo info();
}

interface ServerHostAndPort {
    String host();

    int port();
}

interface ServerInfo {
    String name();
}
application.properties
server.host=localhost
server.port=8080
server.name=konoha

Sem a @WithParentName, o método name() requer a propriedade de configuração server.info.name. Como usamos @WithParentName`, o mapeamento info() herdará o nome pai de Server e name() mapeará para server.name.

1.6.3. namingStrategy

Os nomes de métodos em camelCase são mapeados para nomes de propriedades em kebab-case:

@ConfigMapping(prefix = "server")
public interface Server {
    String theHost();

    int thePort();
}
application.properties
server.the-host=localhost
server.the-port=8080

A estratégia de mapeamento pode ser ajustada com a configuração do valor namingStrategy na anotação @ConfigMapping:

@ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM)
public interface ServerVerbatimNamingStrategy {
    String theHost();

    int thePort();
}
application.properties
server.theHost=localhost
server.thePort=8080

A anotação @ConfigMapping suporta as seguintes estratégias de nomeação com os seguintes valores de enum:

  • KEBAB_CASE (padrão) - O nome do método é derivado substituindo as mudanças de maiúsculas por um traço para mapear a propriedade de configuração, ou seja, theHost mapeia para the-host.

  • VERBATIM - O nome do método é usado como está para mapear a propriedade de configuração, ou seja, theHost mapeia para theHost.

  • SNAKE_CASE - O nome do método é derivado pela substituição das letras maiúsculas por um sublinhado para mapear a propriedade de configuração, ou seja, theHost mapeia para the_host.

1.6.4. beanStyleGetters

The beanStyleGetters attribute (default false) enables matching bean-style getter names (get/is prefixed) to their property name equivalent. For example, getHost() and isEnabled() map to the properties host and enabled respectively:

@ConfigMapping(prefix = "server", beanStyleGetters = true)
public interface Server {
    String getHost();

    int getPort();

    boolean isEnabled();
}
application.properties
server.host=localhost
server.port=8080
server.enabled=true
Bean-style getter matching allows multiple method names to match the same configuration name. For instance, getFoo and isFoo both match foo, which may not be intended. Prefer simple method names that match one-to-one with their configuration names.

1.7. Conversões

Uma classe de mapeamento de configuração suporta conversões automáticas de todos os tipos disponíveis para conversão em 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();
}
application.properties
int=9
long=9999999999
float=99.9
double=99.99
char=c
boolean=true

Isso também é válido para Optional e seus similares:

@ConfigMapping
public interface Optionals {
    Optional<Server> server();

    Optional<String> optional();

    @WithName("optional.int")
    OptionalInt optionalInt();

    interface Server {
        String host();

        int port();
    }
}

Nesse caso, o mapeamento não falhará se não houver nenhuma propriedade de configuração que corresponda ao mapeamento.

1.7.1. @WithConverter

A anotação @WithConverter oferece uma maneira de definir um Converter para ser usado em um mapeamento específico:

@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";
    }
}
application.properties
foo=foo

Uma chamada para Converters.foo(), o valor retornado será bar.

1.7.2. Optionals

A mapping can wrap any complex type with an Optional. Optional mappings do not require the configuration path and value to be present, so no NoSuchElementException is thrown when the configuration property is missing.

1.7.3. Coleções

Um mapeamento de configuração também é capaz de mapear os tipos de coleções List e 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();
        }
    }
}
application.properties
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

Mapeamentos List ou Set podem usar propriedades indexadas para mapear valores de configuração em grupos de mapeamento. Para coleções com tipos de elementos simples , como String, o valor de configuração é uma cadeia de caracteres separada por vírgulas.

A List mapping is backed by an ArrayList, and a Set mapping is backed by a HashSet. Only the List mapping can maintain element order.

1.7.4. Mapas

Um mapeamento de configuração também é capaz de mapear um Map:

@ConfigMapping(prefix = "server")
public interface Server {
    String host();

    int port();

    Map<String, String> form();

    Map<String, List<Alias>> aliases();

    interface Alias {
        String name();
    }
}
application.properties
server.host=localhost
server.port=8080
server.form.index=index.html
server.form.login.page=login.html
server.form.error.page=error.html
server.aliases.localhost[0].name=prod
server.aliases.localhost[1].name=127.0.0.1
server.aliases."io.quarkus"[0].name=quarkus

The configuration property needs to specify an additional segment to act as the map key. In this case the form() Map will contain three elements with the keys index, login.page and error.page.

Quotes are required around a Map key only when the key contains a dot (e.g. "io.quarkus"), because a dot would otherwise be interpreted as a path separator. For single-segment keys (no dots), quotes are optional.

Do not mix quoted and unquoted forms for the same Map key. For a key that spans a single segment, both server.aliases.localhost.name and server.aliases."localhost".name refer to the same key localhost, making it ambiguous. When SmallRye Config finds both forms it resolves the entry using the quoted form and logs a warning.

When both quoted and unquoted forms are found, only the quoted form is then used to look up values and; property names written in the unquoted form are left unmapped and the mapping may fail validation or yield unexpected values. If members of a nested group are split between the two forms, the unquoted members are not found and the mapping may fail validation or yield unexpected values.

Pick one form and use it consistently for a given key across all configuration sources.

Isso também funciona para grupos:

@ConfigMapping(prefix = "server")
public interface Servers {
    @WithParentName
    Map<String, Server> allServers();
}

public interface Server {
    String host();

    int port();

    String login();

    String error();

    String landing();
}
application.properties
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

Nesse caso, o Map allServers() conterá um elemento Server com a chave my-server.

1.7.5. @WithUnnamedKey

The @WithUnnamedKey annotation allows omitting a single map key in the configuration path:

@ConfigMapping(prefix = "server")
public interface Server {
    @WithUnnamedKey("localhost")
    Map<String, Alias> aliases();

    interface Alias {
        String name();
    }
}
application.properties
server.aliases.name=localhost
server.aliases.prod.name=prod

The server.aliases.name property is unnamed because it does not contain the map key segment. Due to @WithUnnamedKey("localhost"), the key localhost is used automatically when the map key is absent.

Server server = config.getConfigMapping(Server.class);
Alias localhost = server.aliases().get("localhost");
Alias prod = server.aliases().get("prod");

If the unnamed key is also explicitly set in a property name (e.g. server.aliases.localhost.name=explicit), the explicit value takes precedence over the unnamed entry.

The eager attribute (default true) controls whether the unnamed key entry is included when its values come only from defaults. When eager = false, the entry is excluded from the Map unless at least one value is explicitly set in a configuration source.

1.7.6. @WithKeys

The @WithKeys annotation defines which Map keys must be loaded by the configuration, instead of discovering keys from Config#getPropertyNames. This is useful when the ConfigSource does not enumerate its properties:

@ConfigMapping(prefix = "server")
public interface Server {
    @WithKeys(KeysProvider.class)
    Map<String, Alias> aliases();

    interface Alias {
        String name();
    }

    class KeysProvider implements Supplier<Iterable<String>> {
        @Override
        public Iterable<String> get() {
            return List.of("dev", "test", "prod");
        }
    }
}

Each key must exist in the final configuration relative to the Map path segment, or the mapping will fail with a ConfigValidationException.

1.7.7. @WithDefaults

The @WithDefaults marker annotation on a Map returns the default value for the value element on any key lookup:

@ConfigMapping(prefix = "server")
public interface Server {
    @WithDefaults
    Map<String, Alias> aliases();

    interface Alias {
        @WithDefault("localhost")
        String name();
    }
}
application.properties
server.aliases.prod.name=prod

A lookup with the key localhost, any, or any other key returns an Alias instance populated from @WithDefault values. A lookup with prod returns an Alias instance with name=prod because the property is defined in the configuration. The Map can only iterate and size explicitly defined keys — in this case only prod.

Server server = config.getConfigMapping(Server.class);
Alias localhost = server.aliases().get("localhost");    (1)
Alias any = server.aliases().get("any");                (2)
Alias prod = server.aliases().get("prod");              (3)
1 Calling localhost.name() returns localhost
2 Calling any.name() also returns localhost, since it is the default
3 Calling prod.name() return prod, since it is the value defined in the configuration file

1.8. Padrões

A anotação @WithDefault permite definir uma propriedade padrão em um mapeamento (e evitar um erro se o valor da configuração não estiver disponível em nenhum ConfigSource):

public interface Defaults {
    @WithDefault("foo")
    String foo();

    @WithDefault("bar")
    String bar();
}

Não são necessárias propriedades de configuração. O Defaults.foo() retornará o valor foo e o Defaults.bar() retornará o valor bar.

1.9. Secrets

A mapping can mark a member as a secret with Secret<T>:

@ConfigMapping(prefix = "credentials")
public interface Credentials {
    String username();

    Secret<String> password();
}

A Secret value modifies the behavior of the Config system by:

  • Omitting the name of the secret from Config#getPropertyNames()

  • Omitting the name and value of the secret from the mapping toString output

  • Throwing a SecurityException when trying to retrieve the value via the Config programmatic API

A Secret can be of any type that can be converted by a registered org.eclipse.microprofile.config.spi.Converter of the same type.

1.10. toString, equals, hashCode

If the config mapping contains a toString method declaration, the config mapping instance will include a proper implementation of the toString method. The equals and hashCode methods are included automatically.

Do not include a toString declaration in a config mapping with sensitive information.

1.11. Validação

Um mapeamento de configuração pode combinar anotações do Bean Validation para validar os valores de configuração:

@ConfigMapping(prefix = "server")
public interface Server {
    @Size(min = 2, max = 20)
    String host();

    @Max(10000)
    int port();
}

The application startup fails with a io.smallrye.config.ConfigValidationException if the configuration property values do not follow the constraints defined in Server.

For validation to work, the quarkus-hibernate-validator extension is required, and it is performed automatically.

1.12. Mocking (Simulação)

Uma implementação de interface de mapeamento não é um proxy, portanto, não pode ser simulada diretamente com @InjectMock como outros beans CDI. Um truque é torná-la proxy com um método produtor:

public class ServerMockProducer {
    @Inject
    Config config;

    @Produces
    @ApplicationScoped
    @io.quarkus.test.Mock
    Server server() {
        return config.unwrap(SmallRyeConfig.class).getConfigMapping(Server.class);
    }
}

O Server pode ser injetado como uma simulação em uma classe de teste do Quarkus com @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.

Se o objetivo for apenas simular determinados valores de configuração e manter a configuração original, a instância de simulação exigirá um spy (espião):

@ConfigMapping(prefix = "app")
@Unremovable
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;
    }
}

O AppConfig pode ser injetado como uma simulação em uma classe de teste do Quarkus com @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.

2. ConfigInstanceBuilder

With the io.smallrye.config.ConfigInstanceBuilder API, it is possible to create instances of a config mapping interface programmatically, without requiring a SmallRyeConfig instance or any configuration source. This is particularly useful for testing, providing default configurations, or any scenario where configuration values are known ahead of time.

The configuration interface does not need the @ConfigMapping annotation to work with the builder. Any valid configuration interface is accepted.

2.1. Usage

A configuration interface instance is created with ConfigInstanceBuilder.forInterface():

interface Server {
    String host();

    int port();
}

Server server = ConfigInstanceBuilder.forInterface(Server.class)
        .with(Server::host, "localhost")
        .with(Server::port, 8080)
        .build();

The builder uses method references to identify which property to set, providing compile-time type safety without string-based property names.

2.2. Primitive Types

The builder provides dedicated with() overloads for int, long, double, and boolean. Other primitive types (byte, short, float, char) use the generic with() method with their boxed types:

@ConfigMapping
interface Primitives {
    int intValue();

    boolean booleanValue();

    byte byteValue();
}

Primitives primitives = ConfigInstanceBuilder.forInterface(Primitives.class)
        .with(Primitives::intValue, 42)
        .with(Primitives::booleanValue, true)
        .with(Primitives::byteValue, Byte.valueOf((byte) 1))
        .build();

2.3. Optional Properties

The withOptional() method sets Optional properties. If an optional property is not set, it defaults to empty:

interface AppConfig {
    Optional<String> name();

    OptionalInt timeout();
}

AppConfig config = ConfigInstanceBuilder.forInterface(AppConfig.class)
        .withOptional(AppConfig::name, "MyApp")
        .withOptional(AppConfig::timeout, 30)
        .build();

The withOptional method wraps the value in Optional.of(), OptionalInt.of(), OptionalLong.of(), or OptionalDouble.of() depending on the property type.

2.4. Padrões

Properties annotated with @WithDefault are automatically applied when no value is explicitly set in the builder:

interface ServerDefaults {
    @WithDefault("localhost")
    String host();

    @WithDefault("8080")
    int port();
}

ServerDefaults server = ConfigInstanceBuilder.forInterface(ServerDefaults.class).build(); (1)
1 Both host() and port() return their @WithDefault values

Explicitly set values override the @WithDefault annotation:

ServerDefaults server = ConfigInstanceBuilder.forInterface(ServerDefaults.class)
        .with(ServerDefaults::host, "0.0.0.0")
        .build(); (1)
1 Now, host() returns 0.0.0.0 and, port() returns 8080 from @WithDefault

2.5. Nested Groups

Nested configuration groups are built separately and composed into the parent builder:

interface AppConfig {
    String name();

    DatabaseConfig database();
}

interface DatabaseConfig {
    String url();

    int poolSize();
}

AppConfig config = ConfigInstanceBuilder.forInterface(AppConfig.class)
        .with(AppConfig::name, "MyApp")
        .with(AppConfig::database, ConfigInstanceBuilder.forInterface(DatabaseConfig.class)
                .with(DatabaseConfig::url, "jdbc:h2:mem:test")
                .with(DatabaseConfig::poolSize, 10)
                .build())
        .build();
If a nested group has @WithDefault values for all its members, the nested group instance is automatically built with those defaults when not explicitly set in the parent builder.

2.6. Collections and Maps

List, Set, and Map types are set directly with their values:

interface AppConfig {
    List<String> hosts();

    Map<String, String> labels();
}

AppConfig config = ConfigInstanceBuilder.forInterface(AppConfig.class)
        .with(AppConfig::hosts, List.of("host1", "host2"))
        .with(AppConfig::labels, Map.of("env", "prod", "region", "us-east"))
        .build();

2.7. Required Properties

Properties without a @WithDefault are considered required. Calling build() throws a NoSuchElementException if any required property is not set:

interface Server {
    String host();

    @WithDefault("8080")
    int port();
}

Server fails = ConfigInstanceBuilder.forInterface(Server.class)
        .build(); (1)

Server works = ConfigInstanceBuilder.forInterface(Server.class)
        .with(Server::host, "localhost")
        .build(); (2)
1 - Throws a NoSuchElementException, the host is required but not set
2 - Works as expected since the host value is now provided

2.8. Builder Reuse

A builder instance can be used to produce multiple independent instances. Each build() call creates a new object:

ConfigInstanceBuilder<Server> builder = ConfigInstanceBuilder.forInterface(Server.class)
        .with(Server::host, "localhost")
        .with(Server::port, 8080);

Server first = builder.build();
Server second = builder.build();
boolean equals = first.equals(second); (1)
1 first and second are equal but not the same object