Contents

Overview

The config system represents configuration as a tree in memory. Many developers will choose to work directly with config values — values from the leaves in the tree — accessing them by their keys. You can also navigate explicitly among the nodes of the tree without using keys. This section describes what the tree looks like and how you can traverse it.

Configuration Node Types

The config system represents configuration in memory using three types of nodes, each a different interface defined within the ConfigNode interface.

ConfigNode Types
TypeJava InterfaceUsage
objectConfigNode.ObjectNodeRepresents complex structure (a subtree). Its child nodes can be of any type.
listConfigNode.ListNodeRepresents a list of nodes. Its components can be of any type.
valueConfigNode.ValueNodeRepresents a leaf node.

A node of any type can have a String value.

Each config tree in memory will have an object node as its root with child nodes as dictated by the source config data from which the config system built the tree.

Missing Config Nodes

If your application attempts to access a non-existent node, for example using

config.get("key.does.not.exist")
Copied

the config system returns a Config node object with type MISSING. The in-memory config tree contains nodes only of types OBJECT, LIST, and VALUE.

Configuration Key

Each config node (except the root) has a non-null key. Here is the formal definition of what keys can be:

The ABNF syntax of config key
config-key = *1( key-token *( "." key-token ) )
 key-token = *( unescaped / escaped )
 unescaped = %x00-2D / %x2F-7D / %x7F-10FFFF
           ; %x2E ('.') and %x7E ('~') are excluded from 'unescaped'
   escaped = "~" ( "0" / "1" )
           ; representing '~' and '.', respectively
Copied

Important

To emphasize, the dot character (“.”) has special meaning as a name separator in keys. To include a dot as a character in a key escape it as “~1”. To include a tilda escape it as “~0”.

In-memory Representation of Configuration

The following example is in HOCON (human-optimized config object notation) format. The config system supports HOCON as an extension module.

HOCON application.conf file
app {
    greeting = "Hello"
    page-size = 20
    basic-range = [ -20, 20 ]
}
data {
    providers: [
        {
            name = "Provider1"
            class = "this.is.my.Provider1"
        },
        {
            name = "Provider2"
            class = "this.is.my.Provider2"
        }
    ]
}
Copied

The diagram below illustrates the in-memory tree for that configuration.

Config Nodes structure of application.conf file
Loaded Config Nodes structure

Notes

  1. Each non-root node has a name which distinguishes it from other nodes with the same parent. The interpretation of the name depends on the node type.
    Node TypeName
    object
    value
    member name of the node within its parent
    listelement index of the node within the containing list
  2. Each node’s key is the fully-qualified path using dotted names from the root to that node.
  3. The root has an empty key, empty name, and no value.

The Config object exposes methods to return the name, key, and type of the node.

Access by Key

For many applications, accessing configuration values by key will be the simplest approach. If you write the code with a specific configuration structure in mind, your code can retrieve the value from a specific configuration node very easily.

Your application can specify the entire navigation path as the key to a single get invocation, using dotted notation to separate the names of the nodes along the path. The code can navigate one level at a time using chained get invocations, each specifying one level of the path to the expected node. Or, you can mix the two styles.

All the following lines retrieve the same Config node.

Equivalent Config Retrievals
assert config.get("") == config;
Config provName1 = config.get("data.providers.0.name"); 
Config provName2 = config.get("data.providers.0").get("name"); 
Config provName3 = config.get("data.providers").get("0.name");
Config provName4 = config.get("data").get("providers.0").get("name");
Config provName5 = config.get("data").get("providers").get("0").get("name"); 
Copied
  • using a single key
  • mixed style (composite key and single key)
  • navigating one level with each get invocation

The Config.get(key) method always returns a Config object without throwing an exception. If the specified key does not exist the method returns a Config node of type MISSING. There are several ways your application can tell whether a given config value exists.

MethodUsage
existsReturns true or false
ifExistsExecute functional operations for present nodes
typeReturns enum value for the Config.Type; Config.Type.MISSING if the node represents a config value that does not exist
asReturns the ConfigValue with the correct type that has all methods of Optional and a few additional ones - see ConfigValue interface.

The config system throws a MissingValueException if the application tries to access the value of a missing node by invoking the ConfigValue.get() method.

Access by General Navigation

Some applications might need to work with configuration without knowing its structure or key names ahead of time, and such applications can use various methods on the Config class to do this.

General Config Node Methods
MethodUsage
asNodeList()Returns a ConfigValue<List<Config>>. For nodes of type OBJECT contains child nodes as a List.
hasValue()For any node reports if the node has a value. This can be true for any node type except MISSING.
isLeaf()Reports whether the node has no child nodes. Leaf nodes have no children and has a single value.
key()Returns the fully-qualified path of the node using dotted notation.
name()Returns the name of the node (the last part of the key).
asNode()Returns a ConfigValue<Config> wrapped around the node
traverse()
traverse(Predicate<Config>)
Returns a Stream<Config> as an iterative deepening depth-first traversal of the subtree
type()Returns the Type enum value for the node: OBJECT, LIST, VALUE, or MISSING
List names of child nodes of an object node
List<String> appNodeNames = config.get("app")
            .asNodeList()                              
            .map(nodes -> {                            
                return nodes
                        .stream()
                        .map(Config::name)
                        .sorted()
                        .collect(Collectors.toList());
            })
            .orElse(Collections.emptyList());          


assert appNodeNames.get(0).equals("basic-range"); 
assert appNodeNames.get(1).equals("greeting");    
assert appNodeNames.get(2).equals("page-size");   
Copied
  • Get the ConfigValue with child Config instances.
  • Map the node list to names using the Java Stream API (if present)
  • Use an empty list if the "app" node does not exist
  • Check that the list contains the expected child names: basic-range, greeting and page-size.
List child nodes of a list node
List<Config> providers = config.get("data.providers")
        .asNodeList().orElse(Collections.emptyList());               

assert providers.get(0).key().toString().equals("data.providers.0"); 
assert providers.get(1).key().toString().equals("data.providers.1"); 
Copied
  • Get child nodes of the data.providers list node as a List of Config instances.
  • Check that the list contains the expected child nodes with keys data.providers.0 and data.providers.1.

The traverse() method returns a stream of the nodes in the subtree that is rooted at the current configuration node. Depending on the structure of the loaded configuration the stream contains a mix of object, list or leaf value nodes.

Traverse subtree below a list node
config.get("data.providers")
        .traverse()                                                             
        .forEach(node -> System.out.println(node.type() + " \t" + node.key())); 
Copied
  • Visit the subtree rooted at the data.providers list node.
  • Prints out following list of nodes (type and key):
OBJECT 	data.providers.0
VALUE 	data.providers.0.name
VALUE 	data.providers.0.class
OBJECT 	data.providers.1
VALUE 	data.providers.1.name
VALUE 	data.providers.1.class

The optional Predicate<Config> argument to the traverse methods allows the application to prune the traversal of a subtree at any point.

Traverse root (object) node, skipping the entire data subtree
config.traverse(node -> !node.name().equals("data"))                            
        .forEach(node -> System.out.println(node.type() + " \t" + node.key())); 
Copied
  • Visit all root sub-nodes, excluding whole data tree structure but including others.
  • Prints out following list of nodes (type and key):
OBJECT 	app
VALUE 	app.page-size
VALUE 	app.greeting
LIST 	app.basic-range
VALUE 	app.basic-range.0
VALUE 	app.basic-range.1

Detaching a Config Subtree

Sometimes it can be convenient to write part of your application to deal with configuration without it knowing if or where the relevant configuration is plugged into a larger config tree.

For example, the application.properties from the introduction section contains several settings prefixed with web such as web.page-size. Perhaps in another config source the same information might be stored as server.web.page-size:

Alternate Structure for Web Config
server.web.page-size: 40
server.web.debug = true
server.web.ratio = 1.4
Copied

You might want to write the web portion of your app to work with a config subtree with keys that are independent of the subtree’s position in a larger tree. This would allow you to reuse the web portion of your application without change, regardless of which structure a config source used.

One easy way to do this is to detach a subtree from a larger config tree. When your application invokes the Config.detach method it gets back a copy of the config node but with no parent. The copy and the original node both point to the same objects for their child nodes (if any). The original node is unchanged.

Detaching a Subtree
Config originalRoot = // from the original example `.conf` file
Config alternateRoot = // from the alternate structure above

Config detachedFromOriginal = originalRoot.get("web").detach();
Config detachedFromAlternate = alternateRoot.get("server.web").detach();

assert originalRoot.get("web.debug").equals("true");          
assert alternateRoot.get("server.web.debug").equals("true");  

assert detachedFromOriginal.get("debug").equals("true");      
assert detachedFromAlternate.get("debug").equals("true");     
Copied
  • Navigation depends on knowing the full structure of the config and so is different for the two cases.
  • Detaching so the web node is the root can use the same key regardless of where the config subtree came from.