Contents

Overview

Although config values are originally text, you can use the config system’s built-in conversions or add your own to translate text into Java primitive types and simple objects (such as Double), into enum values, and to express parts of the config tree as complex types (List, Map, and custom types specific to your application). This section introduces how to use the built-in mappings and your own custom ones to convert to simple and complex types.

Converting Configuration to Simple Types

The Config class itself provides many conversions to Java types. See the JavaDoc for the complete list.

The methods which support Java primitive types and their related classes follow a common pattern. The examples in the table below deal with conversion to a boolean but the same pattern applies to many data types listed in the JavaDoc.

Assume a local variable has been assigned something like

Config config = Config.get("someKey");
// shortcut method
ConfigValue<Boolean> value = config.asBoolean();
// generic method (for any type)
ConfigValue<Boolean> value2 = config.as(Boolean.class);
Copied
Built-in Conversions to Simple Types (e.g., boolean)
Java typeExample usage 1
booleanboolean b = value.get(); 2boolean defaultedB = value.orElse(true); 3
Optional<Boolean>ConfigValue already has all methods of an Optional. If actual optional is needed: Optional<Boolean> b = value.asOptional(); 4
Supplier<Boolean>Boolean b = value.supplier().get();boolean defaultedB = value.supplier(true).get();
Supplier<Optional<Boolean>>Boolean b = value.optionalSupplier().get().orElse(Boolean.TRUE);

Notes on Built-in Conversions to Simple Types

1 All conversions can throw MissingValueException (if no value exists at the requested key and no default is provided) and ConfigMappingException (if some error occurred while performing the data mapping).

2 The Config.asXXX methods internally use the Java-provided XXX.parseXXX methods, so here a missing or un-parseable string gives false because that is how Boolean.parseBoolean behaves.

3 User code defaults the value to true.

4 User code defaults the value to Boolean.TRUE if absent; otherwise parses the value using Boolean.parseBoolean.

The numerous conversions defined on the Config class for other types (integers, doubles, etc.) will satisfy many of your application’s needs. The ConfigMappers class includes other related mappings from String (rather than from Config) to Java types (described in the JavaDoc).

For additional type mapping, you can use these methods defined on Config:

T as(Class<? extends T> type);
T as(Function<Config, T> mapper);
T as(GenericType<T> genericType);
Copied

which maps the current node to a type.

The next example, and later ones below showing complex type mapping, use the example application.properties configuration from the config introduction. Part of that example includes this line:

bl.initial-id = 10000000000
Copied

Your application can use Config.as to interpret the value as a BigDecimal:

BigDecimal initialId = config.get("bl.initial-id").as(BigDecimal.class);
Copied

Converting Configuration to enum Values

Configuration can automatically map Config nodes to most enum types.

Your application code simply passes the enum class type to config.as(Class<? extends T> type). The built-in enum converter attempts to match the string value in the config node to the name of one of the values declared for that specific enum in the Java code.

Matching enum Names

The conversion applies the following algorithm to match config values to enum names, stopping as soon as it finds a match:

  1. Select an exact match if one exists.
  2. Treat hyphens (-) in config strings as underscores (_) and select an otherwise exact match if one exists.
  3. Select a case-insensitive match (with or without hyphen substitution) if there is exactly one such match.
  4. Finding no match or multiple case-insensitive matches, throw a ConfigMappingException.

Example

The following example illustrates how to use the built-in enum conversion feature. The example code builds a simple Config tree itself which contains simple test data; normally your application would load the config from a file or some other location.

class Example {

    enum Color {RED, YELLOW, BLUE_GREEN};

    void convert() {
        Config config = Config.just(ConfigSources.create(Map.of("house.tint", "blue-green",
                                                                "car.color", "Red",
                                                                "warning", "YELLOW")));

        Color house = config.get("house.tint") 
                            .as(Color.class)   
                            .get();            
        Color car = config.get("car.color")
                          .as(Color.class)
                          .get();              
        Color warning = config.get("warning")
                              .as(Color.class)
                              .get();          
    }
}
Copied
  • Retrieve the Config object corresponding to the key house.tint.
  • Indicate that, when the value in that Config object is converted, Helidon should convert it to a Color enum value.
  • Convert and retrieve the value.
  • The config key car.color locates the mixed-case string Red which the converter matches to Color.RED.
  • The config key warning locates YELLOW which the converter matches exactly to Color.YELLOW.

Why use heuristics in matching strings to enum values?

Short answer: ease-of-use.

Users composing config sources often adopt a style with hyphens within words to improve readability and lower-case keys and values. With that style in mind, users typing an enum value into a config source might accidentally enter a hyphen instead of an underscore or use lower case instead of upper case. Users might even prefer to make these changes so they can follow their preferred config style.

With the heuristics, Helidon allows users to adopt a common config style and prevents unnecessary runtime exceptions—​and user frustration—​from inconsequential typos.

Remember:

  • Helidon always finds exact matches unambiguously, without relying on the heuristics. In our Color example the text BLUE_GREEN always maps to Color.BLUE_GREEN.

  • Because hyphens cannot appear in a valid Java enum value name, interpreting them as underscores during enum conversion introduces no ambiguity.

Only in the following unusual sitatuation are the heuristics unable to unambiguously match a string to an enum value:

  • The enum has values which differ only in their case (such as Red and RED), and

  • The string in the config source is not an exact match with an enum value name (such as red).

If your application must deal with such cases, write your own function which maps a Config node to the correct enum value, resolving the ambiguities however makes sense in your use case. Your code tells config to use that function instead of the built-in enum conversion when it converts values. A later section describes this technique which works for all types, not only enum types.

Converting Configuration to Complex Types

The hierarchical features section describes the tree structure used to represent config data. The config system can map subtrees of a config tree to complex Java types.

Built-in Conversions to List and Map

The Config class exposes several methods for mapping a structured config node to a Java List or Map. The JavaDoc contains complete details, but briefly your application can convert a structured Config node into:

  • a List<T> of a given type

  • a Map<String, String> in which each key is the fully-qualified key String for a config entry and the value is its String value

Custom Conversions

Often your code will be simpler if you can treat parts of the configuration as custom, application-specific Java objects, rather than as a group of String keys and values. You will need customized conversions to do so.

The config system provides many ways to accomplish this, described in the io.helidon.config package JavaDoc.

Some of those approaches require that the target class — the class to which you want to convert the configuration data — have certain characteristics or that you add a method to the class to help do the mapping. You might want to avoid changing the target class else you might not even be able to if you do not control its source.

Here are two approaches that will always work without requiring changes to the target class. For both approaches, you write your own conversion function. The difference is in how your application triggers the use of that mapper.

Use Custom Mapper Explicitly: Config.as method

Any time your application has a Config instance to map to the target class it invokes Config.as passing an instance of the corresponding conversion function:

Config config = Config.get("web");
ConfigValue<WebConfig> web = config.as(WebConfigMapper::map);
Copied

You do not necessarily need a new instance of the mapper every time you want to use it.

In this approach, everywhere your application needs to perform this conversion it specifies the mapper to use. If you decided to change which mapper to use you would need to update each of those places in your application.

Register Custom Mapper Once, Use Implicitly: Config.as method

In this approach, your application:

  1. Tells each Config.Builder that needs to know about the custom mapper by either:
    1. registering an instance of your mapper by invoking Config.Builder.addMapper, or
    2. implementing ConfigMapperProvider so it returns an instance of your mapper (see the JavaDoc for complete information) and creating or editing the file io.helidon.config.spi.ConfigMapperProvider so it contains a line with the fully-qualified class name of your ConfigMapperProvider. The config system will use the Java service loader to find and invoke all ConfigMapperProvider classes listed and add the mappers they provide to each Config.Builder automatically.
  2. Converts using the mapper by invoking the Config.as method which accepts the target type to convert to, not the mapper itself that does the conversion.

If your application converts to the same target type in several places in the code, this approach allows you to change which mapper it uses by changing only the registration of the mapper, not each use of it.

Continuing the Web Example

The following examples build on the example configuration from the application.properties example file in the introduction.

Java POJO to Hold web Properties Config
public class WebConfig {
    private boolean debug;
    private int pageSize;
    private double ratio;

    public WebConfig(boolean debug, int pageSize, double ratio) {
        this.debug = debug;
        this.pageSize = pageSize;
        this.ratio = ratio;
    }

    public boolean isDebug() {
        return debug;
    }

    public int getPageSize() {
        return pageSize;
    }

    public double getRatio() {
        return ratio;
    }
}
Copied
Custom Mapper Class
public class WebConfigMapper implements Function<Config, WebConfig> {

        @Override
        public WebConfig apply(Config config) {
            return new WebConfig(
                    config.get("debug").asBoolean().orElse(false),
                    config.get("page-size").asInt().orElse(10),
                    config.get("ratio").asDouble().orElse(1.0)
            );
        }
    }
Copied
Explicitly Using the Mapper
Config config = Config.create(classpath("application.properties"));

WebConfig web = config.get("web")
    .as(new WebConfigMapper())
    .get();
Copied
Registering and Implicitly Using the Mapper
Config config = Config.builder(classpath("application.properties"))
    .addMapper(WebConfig.class, new WebConfigMapper())
    .build();

WebConfig web = config.get("web")
    .as(WebConfig.class)
    .get();
Copied

Either of the two approaches just described will always work without requiring you to change the POJO class.

Advanced Conversions using Explicit Mapping Logic

If the target Java class you want to use meets certain conditions — or if you can change it to meet one of those conditions — you might not need to write a separate mapper class. Instead, you add the mapping logic to the POJO itself in one of several ways and the config system uses Java reflection to search for those ways to perform the mapping.

Your application facilitates this implicit mapping either by adding to the POJO class or by providing a builder class for it.

This feature is available in Object mapping module, and is added through Java ServiceLoader mechanism. This is no longer part of core Config module, as it depends on reflection and introduces a lot of magic (see the list of supported mapping methods below, also uses reflection to invoke the methods and to map configuration values to fields/methods etc.).

Config object mapping Dependency in pom.xml
<dependencies>
    <dependency>
        <groupId>io.helidon.config</groupId>
        <artifactId>helidon-config-object-mapping</artifactId>
    </dependency>
</dependencies>
Copied

Adding the Mapping to the POJO

If you can change the target class you can add any one of the following methods or constructors to the POJO class which the config system will find and use for mapping.

Continuing with the WebConfig example introduced earlier:

Methods Supporting Auto-mapping
static WebConfig create(Config);
static WebConfig from(Config);
static WebConfig from(String);
static WebConfig of(Config);
static WebConfig of(String);
static WebConfig valueOf(Config);
static WebConfig valueOf(String);
static WebConfig fromConfig(Config);
static WebConfig fromString(String);
Constructors Supporting Auto-mapping
WebConfig(Config);
WebConfig(String);

If the config system finds any of these methods or constructors when the application invokes

WebConfig wc = config.as(WebConfig.class).get();
Copied

it will invoke the one it found to map the config data to a new instance of the target class. You do not need to write a separate class to do the mapping or register it with the Config.Builder for the config instance.

Writing a Builder Method and Class for the POJO

You can limit the changes to the POJO class by adding a single builder method to the POJO which returns a builder class for the POJO:

public class WebConfig {
    static WebConfigBuilder builder() {
        return new WebConfigBuilder();
    }
}
Copied

The builder class WebConfigBuilder is expected to be a Java Bean with

  1. bean properties named for the config properties of interest, and
  2. a method WebConfig build() which creates the mapped instance from the builder’s own bean properties.

When your application invokes config.as(WebConfig.class) the config system

  1. finds and invokes the WebConfig.builder() method,
  2. assigns the bean properties on the returned builder from the config subtree rooted at config, and
  3. invokes the builder’s build() method yielding the resulting WebConfig instance.

Conversions using JavaBean Deserialization

The config system can also interpret your classes as JavaBeans and use the normal bean naming conventions to map configuration data to your POJO classes, using one of these patterns:

  1. POJO as JavaBean - The config system treats the target class itself as a JavaBean, assigning values from the config to the bean properties of the POJO class.
  2. builder as JavaBean - The config system invokes the POJO’s builder() method to obtain a builder for that POJO type and treats the builder class as a JavaBean, assigning values from the config to the builder’s bean properties and then invoking the builder’s build method to create an instance of the target POJO class.
  3. POJO with factory method or decorated constructor - The config system finds a from method or a constructor on the POJO class itself which accepts annotated arguments, then invokes that method or constructor passing the specified arguments based on the config. The from method returns an instance of the POJO class initialized with the values passed as arguments.

The following sections describe these patterns in more detail.

This feature is available in Object mapping module, and is added through Java ServiceLoader mechanism. This is no longer part of core Config module, as it depends on reflection.

Config object mapping Dependency in pom.xml
<dependencies>
    <dependency>
        <groupId>io.helidon.config</groupId>
        <artifactId>helidon-config-object-mapping</artifactId>
    </dependency>
</dependencies>
Copied

POJO as JavaBean

If your POJO target class is already a JavaBean — or you can modify it to become one — you might be able to avoid writing any explicit mapping code yourself.

The config system invokes the no-args constructor on the target class to create a new instance. It treats each public setter method and each public non-final field as a JavaBean property. The config system processes any non-primitive property recursively as a JavaBean. In this way the config system builds up the target object from the config data.

By default, the system matches potential JavaBean property names with config keys in the configuration.

Use the Value annotation to control some of JavaBean processing for a given property.

Value Annotation
AttributeUsage
keyIndicates which config key should match this JavaBean property
withDefaultString used for the bean property default value if none is set in the config
withDefaultSupplierSupplier of the default bean property value if not is set in the config

To exclude a bean property from the config system bean processing annotate it with Config.Transient.

Here is an example using the app portion of the example configuration from the introduction.

Java bean to load app properties into via setters
public class AppConfig {
    private Instant timestamp;
    private String greeting;
    private int pageSize;
    private List<Integer> basicRange;

    public AppConfig() {                                          
    }

    public void setGreeting(String greeting) {                    
        this.greeting = greeting;
    }
    public String getGreeting() {
        return greeting;
    }

    @Value(key = "page-size",                              
                  withDefault = "10")                             
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
    public int getPageSize() {
        return pageSize;
    }

    @Value(key = "basic-range",                            
                  withDefaultSupplier = BasicRangeSupplier.class) 
    public void setBasicRange(List<Integer> basicRange) {
        this.basicRange = basicRange;
    }
    public List<Integer> getBasicRange() {
        return basicRange;
    }

    @Config.Transient                                             
    public void setTimestamp(Instant timestamp) {
        this.timestamp = timestamp;
    }
    public Instant getTimestamp() {
        return timestamp;
    }

    public static class BasicRangeSupplier
            implements Supplier<List<Integer>> {                  
        @Override
        public List<Integer> get() {
            return List.of(-10, 10);
        }
    }
}
Copied
  • Public no-parameter constructor.
  • Property greeting is not customized and will be set from the config node with the key greeting, if present in the config.
  • Property pageSize is matched to the config key page-size.
  • If the page-size config node does not exist, the pageSize bean property defaults to 10.
  • Property basicRange is matched to the config key basic-range.
  • If the basic-range config node does not exist, a BasicRangeSupplier instance will provide the default value.
  • The timestamp bean property is never set, even if the config contains a node with the key timestamp.
  • BasicRangeSupplier is used to supply the List<Integer> default value.

Here is an example of code loading config and mapping part of it to the AppConfig bean above.

Map app config node into AppConfig class
Config config = Config.create(classpath("application.conf"));

AppConfig app = config.get("app")
        .as(AppConfig.class)
        .get();                               

//assert that all values are loaded from file
assert app.getGreeting().equals("Hello");
assert app.getPageSize() == 20;
assert app.getBasicRange().size() == 2
        && app.getBasicRange().get(0) == -20
        && app.getBasicRange().get(1) == 20;

//assert that Transient property is not set
assert app.getTimestamp() == null;                          
Copied
  • The config system finds no registered ConfigMapper for AppConfig and so applies the JavaBean pattern to convert the config to an AppConfig instance.
  • Because the bean property timestamp was marked as transient, the config system did not set it.

Builder as JavaBean

If the target class includes the public static method builder() that returns any object, then the config system will make sure that the return type has a method build() which returns an instance of the target class. If so, the config system treats the builder as a JavaBean and

  1. invokes the builder() method to instantiate the builder class,
  2. treats the builder as a JavaBean and maps the Config subtree to it,
  3. invokes the builder’s build() method to create the new instance of the target class.

You can augment the target class with the public static builder() method:

JavaBean for app properties, via a Builder
public class AppConfig {
    private String greeting;
    private int pageSize;
    private List<Integer> basicRange;

    private AppConfig(String greeting, int pageSize, List<Integer> basicRange) { 
        this.greeting = greeting;
        this.pageSize = pageSize;
        this.basicRange = basicRange;
    }

    public String getGreeting() {
        return greeting;
    }

    public int getPageSize() {
        return pageSize;
    }

    public List<Integer> getBasicRange() {
        return basicRange;
    }

    public static Builder builder() {                                            
        return new Builder();
    }

    public static class Builder {                                                
        private String greeting;
        private int pageSize;
        private List<Integer> basicRange;

        private Builder() {
        }

        public void setGreeting(String greeting) {                               
            this.greeting = greeting;
        }

        @Value(key = "page-size",
                      withDefault = "10")
        public void setPageSize(int pageSize) {                                  
            this.pageSize = pageSize;
        }

        @Value(key = "basic-range",
                      withDefaultSupplier = BasicRangeSupplier.class)
        public void setBasicRange(List<Integer> basicRange) {                    
            this.basicRange = basicRange;
        }

        public AppConfig build() {                                               
            return new AppConfig(greeting, pageSize, basicRange);
        }
    }
}
Copied
  • The target class’s constructor can be private in this case because new instances are created from the inner class Builder which has access to `AppConfig’s private members.
  • The target class contains public static method builder() which returns an object that itself exposes the method AppConfig build(), so the config system recognizes it.
  • The config system treats the AppConfig.Builder (not the enclosing target class) as a JavaBean.
  • The builder’s property greeting is not customized and is set from config node with greeting key, if one exists.
  • The builder’s property pageSize maps to the config key page-size and defaults to 10 if absent.
  • The builder’s property basicRange maps to the config key basic-range and uses a BasicRangeSupplier instance to get a default value if needed.
  • Finally, the config system invokes the builder’s public method build(), creating the new instance of AppConfig for use by the application.

Target Class with Annotated Factory Method or Constructor

Another option is to annotate the parameters to a factory method or to a constructor on the target class. You can add a factory method to the target class, a public static method from with parameters annotated to link them to the corresponding config keys. Or you can add or modify a constructor with parameters, similarly annotated to form the link from each parameter to the corresponding config key.

Warning

Be sure to annotate each parameter of the from method or constructor with @Value and specify the key to use for the mapping. The parameter names in the Java code are not always available at runtime to map to config keys. (They might be arg0, arg1, etc.)

Target Class with Factory Method from
public class AppConfig {
    private final String greeting;
    private final int pageSize;
    private final List<Integer> basicRange;

    private AppConfig(String greeting, int pageSize, List<Integer> basicRange) { 
        this.greeting = greeting;
        this.pageSize = pageSize;
        this.basicRange = basicRange;
    }

    public String getGreeting() {
        return greeting;
    }

    public int getPageSize() {
        return pageSize;
    }

    public List<Integer> getBasicRange() {
        return basicRange;
    }

    public static AppConfig from(                                                
            @Value(key = "greeting")
                    String greeting,                                             
            @Value(key = "page-size",
                          withDefault = "10")
                    int pageSize,
            @Value(key = "basic-range",
                          withDefaultSupplier = BasicRangeSupplier.class)
                    List<Integer> basicRange) {
        return new AppConfig(greeting, pageSize, basicRange);
    }
}
Copied
  • The target class constructor can be private because the factory method on the same class has access to it.
  • The config system invokes the factory method from(…​), passing arguments it has fetched from the correspondingly-named config subtrees. The factory method returns the new initialized AppConfig instance. Note the consistent use of @Value(key = "…​") on each parameter.
  • Because the property greeting does not specify a default value the property is mandatory and must appear in the configuration source. Otherwise, the config system throws a ConfigMappingException.

Alternatively, you can use an annotated constructor instead of a static factory method. Revising the example above, make the constructor public, annotate its parameters, and remove the now-unneeded from factory method.

Target Class with Annotated Public Constructor
public class AppConfig {
    public AppConfig( 
        @Value(key = "greeting") 
                String greeting,
        @Value(key = "page-size",
                          withDefault = "10")
                int pageSize,
        @Value(key = "basic-range",
                          withDefaultSupplier = BasicRangeSupplier.class)
                List<Integer> basicRange) {
        this.greeting = greeting;
        this.pageSize = pageSize;
        this.basicRange = basicRange;
    }
}
Copied
  • Constructor is public.
  • Each parameter has the ConfigValue annotation to at least specify the config key name.

When the application invokes config.as(AppConfig.class), the config system locates the public annotated constructor and invokes it, passing as arguments the data it fetches from the configuration matching the annotation key names with the configuration keys.