How to Create Custom Converter Implementations for Property Types in Owner

To create custom Converter implementations for property types in the Owner library, implement the org.aeonbits.owner.Converter<T> interface with a stateless class and annotate your configuration method with @ConverterClass to bind your deserialization logic.

The Owner library (matteobaccan/owner) automates the mapping between property files and Java interfaces using built-in type converters. When your application requires complex domain objects or non-standard string formats that exceed default capabilities, custom Converter implementations for property types provide a clean extension point for bespoke transformation logic.

The Converter Interface and @ConverterClass Annotation

Owner defines the conversion contract through two core components located in the source tree.

The Converter Functional Interface

The Converter<T> interface in owner/src/main/java/org/aeonbits/owner/Converter.java declares the single method that handles all custom transformations:

T convert(Method method, String input);

Your implementation receives the Method object representing the invoked configuration interface method (allowing metadata inspection) and the raw property value as a String. The method must return the converted object of type T, or null if conversion fails.

The @ConverterClass Annotation

Defined in owner/src/main/java/org/aeonbits/owner/Config.java (lines 45-55), the @ConverterClass annotation binds a specific converter implementation to a configuration interface method. The annotation accepts a Class object that must satisfy three constraints:

  • Public visibility
  • Non-abstract concrete implementation
  • Accessible no-argument constructor

When applied to methods returning arrays or collections, Owner applies the converter to each individual element.

Converter Lifecycle and Stateless Design

Understanding the instantiation model is critical for correct implementation. Owner creates a new converter instance for every property access using converterClass.newInstance(). This design enforces thread safety but prohibits stateful converters.

Because each invocation receives a fresh instance, you must avoid mutable instance fields or expensive initialization routines inside the converter. If your conversion requires heavy resources (such as DateTimeFormatter instances or compiled regex patterns), declare them as static final constants or use static utility methods.

Practical Code Examples

The Owner test suite and examples module demonstrate several real-world converter patterns.

Converting Host:Port Strings to Server Objects

The ServerConverter implementation in owner/src/test/java/org/aeonbits/owner/typeconversion/ConverterClassTest.java (lines 57-66) parses server connection strings:

public static class ServerConverter implements Converter<Server> {
    @Override
    public Server convert(Method method, String text) {
        String[] parts = text.split(":", -1);
        String name = parts[0];
        int port = (parts.length >= 2) ? Integer.parseInt(parts[1]) : 80;
        return new Server(name, port);
    }
}

Bind this converter to your configuration interface:

interface MyConfig extends Config {
    @DefaultValue("foobar.com:8080")
    @ConverterClass(ServerConverter.class)
    Server server();
}

Parsing Key-Value Pairs into Map Structures

For complex property formats, the MapPropertyConverter in owner/src/test/java/org/aeonbits/owner/examples/MapPropertyExample.java (lines 32-44) converts comma-separated key-value entries into a LinkedHashMap:

public static class MapPropertyConverter implements Converter<Map<String,String>> {
    @Override
    public Map<String,String> convert(Method method, String input) {
        Map<String,String> map = new LinkedHashMap<>();
        for (String entry : input.split(",", -1)) {
            String[] kv = entry.split(":", -1);
            map.put(kv[0].trim(), kv[1].trim());
        }
        return map;
    }
}

Usage with array returns:

interface MyConfig extends Config {
    @Separator(";")
    @DefaultValue(
        "name : Dante Alighieri, book : Divine Comedy;" +
        "name : Alessandro Manzoni, book : The Betrothed")
    @ConverterClass(MapPropertyConverter.class)
    Map<String,String>[] authors();
}

Integrating Java 8 Date/Time Types

While Owner lacks built-in JSR-310 support, a custom converter bridges the gap:

public class LocalDateConverter implements Converter<LocalDate> {
    private static final DateTimeFormatter FMT = DateTimeFormatter.ISO_LOCAL_DATE;

    @Override
    public LocalDate convert(Method method, String input) {
        return LocalDate.parse(input, FMT);
    }
}

Configuration usage:

interface MyConfig extends Config {
    @DefaultValue("2023-10-01")
    @ConverterClass(LocalDateConverter.class)
    LocalDate startDate();
}

Overriding Built-in Conversions

Custom converters take precedence over default handlers. The OverridesIntegerConversion test case in ConverterClassTest.java (lines 93-97) demonstrates replacing standard Integer parsing:

public static class OverridesIntegerConversion implements Converter<Integer> {
    @Override
    public Integer convert(Method method, String input) {
        return 42;  // Ignores input, returns constant
    }
}
interface MyConfig extends Config {
    @DefaultValue("10")
    @ConverterClass(OverridesIntegerConversion.class)
    int overridden();
}

Key Source Files for Reference

Review these canonical source files to understand the complete converter architecture:

Summary

  • Implement org.aeonbits.owner.Converter<T> to define custom transformation logic from String to any Java type.
  • Annotate configuration methods with @ConverterClass(YourConverter.class) to bind the implementation; the converter must be public, concrete, and have a no-arg constructor.
  • Design converters as stateless objects because Owner instantiates a new instance for every property access via reflection.
  • Use the Method parameter in convert() to inspect configuration method metadata when needed.
  • Place expensive resources (formatters, patterns) in static final fields rather than instance variables to avoid initialization overhead.

Frequently Asked Questions

What are the exact requirements for a custom Converter class?

Your converter class must be public, non-abstract, and possess an accessible no-argument constructor. It must implement the Converter<T> interface from owner/src/main/java/org/aeonbits/owner/Converter.java. Owner instantiates the class using newInstance() for each conversion, so the implementation cannot rely on constructor injection or instance state.

Can I use stateful converters with cached resources?

No. Because Owner creates a new converter instance for every method invocation, instance fields would reset each time. For expensive immutable resources like DateTimeFormatter or compiled regular expressions, declare them as static final constants within the converter class. This provides shared access across all instantiations while maintaining thread safety.

How do I apply custom converters to collections or arrays?

When you annotate a method returning an array or collection with @ConverterClass, Owner automatically applies your converter to each element individually rather than to the entire collection as a single string. Ensure your converter handles the element type (not the collection type), and use @Separator to define how Owner splits the raw property value into individual element strings before conversion.

Can custom converters override Owner's built-in type handling?

Yes. The @ConverterClass annotation takes precedence over default converters. Even if Owner provides a standard converter for types like Integer, Boolean, or URI, annotating the method with your custom implementation completely replaces the built-in behavior. This is demonstrated in ConverterClassTest.java (lines 93-97) where an integer method always returns 42 regardless of the property value.

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 →