How to Use Variable Expansion with ${variable} Syntax in OWNER Property Values

OWNER (matteobaccan/owner) supports variable expansion that substitutes ${variable} placeholders with values from merged environment variables, system properties, and user-defined maps, enabling dynamic configuration in both property files and Java annotations.

OWNER is a Java configuration mapping library that eliminates boilerplate when loading properties. Its variable expansion feature allows you to reference other configuration keys, system properties, or environment variables using the ${variable} syntax directly inside @DefaultValue strings, @Key annotations, and external property files.

How Variable Expansion Works in OWNER

According to the source code in owner/src/main/java/org/aeonbits/owner/VariablesExpander.java, the library constructs a merged lookup map before performing string substitution. When you call ConfigFactory.create(), the framework builds a ConfigImpl instance and initializes a VariablesExpander that combines three sources in priority order—first match wins:

  1. Environment variables from System.getenv()
  2. System properties from System.getProperties()
  3. User-supplied Properties passed as arguments to ConfigFactory.create()

The expander creates a StrSubstitutor (implemented in owner/src/main/java/org/aeonbits/owner/StrSubstitutor.java) backed by this merged map. It calls StrSubstitutor.replace() on every string requiring expansion, including values in @DefaultValue annotations, @Key annotations (supported since Owner 1.0.6), and property files. The expander also handles tilde (~) expansion for user home directories via Util.expandUserHome before delegating to the substitutor.

Configuring Property Files with ${variable} Syntax

You can nest references within .properties files to build composite values dynamically. The ${variable} placeholders resolve against other properties defined in the same file or external sources.


# src/test/resources/example.properties

story=The ${animal} jumped over the ${target}
animal=quick ${color} fox
target=${target.attribute} dog
target.attribute=lazy
color=brown

When loaded through a mapped interface, the ${animal} and ${target} references resolve recursively, producing the final interpolated string.

Using ${variable} in @DefaultValue Annotations

Define dynamic default values directly in your configuration interface using the ${variable} syntax. The VariablesExpander processes these annotations when generating the configuration proxy.

// src/test/java/org/aeonbits/owner/variableexpansion/StoryConfig.java
public interface StoryConfig extends Config {

    @DefaultValue("The ${animal} jumped over the ${target}")
    String story();

    @DefaultValue("quick ${color} fox")
    String animal();

    @DefaultValue("${target.attribute} dog")
    String target();

    @DefaultValue("lazy")
    @Key("target.attribute")
    String targetAttribute();

    @DefaultValue("brown")
    String color();
}
StoryConfig cfg = ConfigFactory.create(StoryConfig.class);
System.out.println(cfg.story());   // → The quick brown fox jumped over the lazy dog

The expansion engine resolves ${target.attribute} before substituting it into the ${target} placeholder, demonstrating recursive variable resolution within the same configuration instance.

Dynamic Key Names with @Key Variable Expansion

Since version 1.0.6, OWNER supports variable expansion inside @Key annotations, allowing runtime determination of which property key to look up. This enables environment-specific configuration structures without changing interface code.

// src/test/java/org/aeonbits/owner/variableexpansion/KeyExpansionExample.java
@Sources("classpath:org/aeonbits/owner/variableexpansion/KeyExpansionExample.xml")
public interface ExpandsFromAnotherKey extends Config {

    @DefaultValue("dev")
    String env();

    @Key("servers.${env}.name")
    String name();

    @Key("servers.${env}.hostname")
    String hostname();

    @Key("servers.${env}.port")
    Integer port();

    @Key("servers.${env}.user")
    String user();

    @Key("servers.${env}.password")
    String password();
}

Inject runtime values by supplying a Map when creating the config instance:

Map<String, String> vars = new HashMap<>();
vars.put("env", "uat");

ExpandsFromAnotherKey cfg = ConfigFactory.create(ExpandsFromAnotherKey.class, vars);
System.out.println(cfg.name());   // → User Acceptance Test

The ${env} placeholder in each @Key resolves before the underlying XML source is queried, effectively switching the property namespace based on the injected variable.

Injecting System Properties and Environment Variables

Reference system properties and environment variables directly using the ${variable} syntax. The VariablesExpander automatically includes System.getenv() and System.getProperties() in its lookup chain.

public interface SystemExample extends Config {

    @DefaultValue("Welcome, ${user.name}")
    String welcome();

    @DefaultValue("${TMPDIR}/tempFile.tmp")
    File tempFile();
}
System.setProperty("user.name", "Alice");

SystemExample cfg = ConfigFactory.create(SystemExample.class,
                                        System.getProperties(),
                                        System.getenv());

System.out.println(cfg.welcome());   // → Welcome, Alice

Pass System.getProperties() and System.getenv() explicitly to ConfigFactory.create() to ensure the expander can resolve standard placeholders like ${user.name}, ${java.io.tmpdir}, or custom environment variables such as ${TMPDIR}.

Disabling Variable Expansion with @DisableFeature

If you need literal ${variable} text without substitution, annotate the interface or specific method with @DisableFeature(VARIABLE_EXPANSION). This prevents the VariablesExpander from processing the string.

public interface NoExpansion extends Config {

    @DisableFeature(VARIABLE_EXPANSION)
    @DefaultValue("Hello ${world}.")
    String greet();          // returns the literal text
}
NoExpansion cfg = ConfigFactory.create(NoExpansion.class);
System.out.println(cfg.greet());   // → Hello ${world}.

Summary

  • Variable expansion uses ${variable} syntax and is handled by VariablesExpander using a merged map of environment variables, system properties, and user-supplied properties.
  • Resolution follows a "first match wins" priority: environment variables override system properties, which override explicitly passed Properties objects.
  • The feature works in @DefaultValue strings, property file values, and @Key annotations (since Owner 1.0.6), enabling both value interpolation and dynamic key selection.
  • Disable expansion per-method or per-interface using @DisableFeature(VARIABLE_EXPANSION) when literal ${} text is required.
  • Runtime injection is achieved by passing a Map<String, String> to ConfigFactory.create(), allowing dynamic configuration without recompilation.

Frequently Asked Questions

Can I use ${variable} syntax in @Key annotations?

Yes, OWNER has supported variable expansion in @Key annotations since version 1.0.6. The VariablesExpander processes the ${variable} placeholders in the annotation value before querying the underlying properties source, allowing dynamic key names like servers.${env}.hostname.

What is the variable resolution priority in OWNER?

The resolution order follows a strict hierarchy implemented in VariablesExpander.java: first environment variables (System.getenv()), then system properties (System.getProperties()), and finally user-supplied Properties passed to ConfigFactory.create(). The first source containing the variable name wins.

How do I disable variable expansion for specific configuration methods?

Annotate the method (or the entire interface) with @DisableFeature(VARIABLE_EXPANSION). This instructs the ConfigFactory to skip the VariablesExpander step for that method, returning the raw string including literal ${variable} text instead of attempting substitution.

Can I inject runtime values for ${variable} placeholders?

Yes, provide a Map<String, String> or Properties object as an argument to ConfigFactory.create(MyConfig.class, myMap). The map entries are merged into the variable lookup context with lower priority than system properties, allowing you to override or define placeholder values at runtime without modifying system environment variables.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →