Built-in converters

Ravel provides converters (priority 1) for these types, automatically applied during config.getValue() and config.getOptionalValue():

Type Behavior

String

Pass-through (no conversion)

boolean, Boolean

Truthy values: true, 1, yes, y, on (case-insensitive) → true; all others → false

byte, short, int, long, Byte, Short, Integer, Long

Via Integer.parseInt(), Long.parseLong(), etc.

float, double, Float, Double

Via Float.parseFloat(), Double.parseDouble()

char, Character

Single-character string; longer values throw IllegalArgumentException

java.net.URL

Via new URL(string)

java.net.URI

Via URI.create(string)

java.net.InetAddress

Via InetAddress.getByName(string)

OptionalInt, OptionalLong, OptionalDouble

Parses the numeric value and wraps it (Optional<T> itself is served by getOptionalValue(), not by a converter)

java.time.Duration

ISO-8601 format: PT1H30M, PT45S, 1800000ms, etc.

java.time.Period

ISO-8601 format: P1Y2M3D, P30D, etc.

java.time.LocalDate

ISO format: 2024-05-13

java.time.LocalTime

ISO format: 14:30:00

java.time.LocalDateTime

ISO format: 2024-05-13T14:30:00

java.time.OffsetTime

ISO format: 14:30:00+02:00

java.time.OffsetDateTime

ISO format: 2024-05-13T14:30:00+02:00

java.time.ZonedDateTime

ISO format: 2024-05-13T14:30:00+02:00[Europe/Paris]

java.time.Instant

ISO format: 2024-05-13T12:30:00Z

java.lang.Class<?>

Via Class.forName(string)

Enum types are converted too (matched by enum constant name), but through the implicit-converter mechanism described below, not by a registered built-in. Collection types have no converter: List<T> values come from the getValues() / getOptionalValues() methods (comma split, built on the array conversion path), and List<T> / Set<T> injection is handled by the CDI layer.

Arrays

// microprofile-config.properties
app.items=foo,bar,baz
app.ids=1,2,3,4

// Code
String[] items = config.getValue("app.items", String[].class);
// Result: ["foo", "bar", "baz"]

Integer[] ids = config.getValue("app.ids", Integer[].class);
// Result: [1, 2, 3, 4]

Collections

getValue() does not accept collection classes — config.getValue("app.items", List.class) throws, because no converter exists for List. Multi-valued properties are read through the dedicated methods:

List<String> items = config.getValues("app.items", String.class);
Optional<List<Integer>> ids = config.getOptionalValues("app.ids", Integer.class);

In CDI beans, List<T> and Set<T> can be injected directly with @ConfigProperty (comma split plus element-wise conversion).

Implicit converters

If no registered converter matches a type, Ravel falls back to an implicit converter (MicroProfile Config §5.2):

  1. Public constructor taking String: new MyType(string)

  2. Static method valueOf(String): MyType.valueOf(string)

  3. Static method parse(CharSequence): MyType.parse(charseq)

public class MyDuration {
    private final long millis;

    // This public constructor enables implicit conversion
    public MyDuration(String iso8601) {
        this.millis = Duration.parse("PT" + iso8601).toMillis();
    }
}

// microprofile-config.properties
app.timeout=1H30M

// Code
MyDuration timeout = config.getValue("app.timeout", MyDuration.class);
// Internally calls: new MyDuration("1H30M")

Custom converters

Register a custom Converter<T> for types not covered by built-in or implicit converters:

import org.eclipse.microprofile.config.spi.Converter;

public class ColorConverter implements Converter<Color> {

    @Override
    public Color convert(String value) {
        if (value == null || value.isEmpty()) {
            throw new IllegalArgumentException("Color cannot be empty");
        }

        if (value.startsWith("#")) {
            // Hex format: #RRGGBB
            return Color.decode(value);
        } else if (value.equalsIgnoreCase("red")) {
            return Color.RED;
        } else if (value.equalsIgnoreCase("green")) {
            return Color.GREEN;
        } else if (value.equalsIgnoreCase("blue")) {
            return Color.BLUE;
        }

        throw new IllegalArgumentException("Unknown color: " + value);
    }
}

Register via ServiceLoader

Create META-INF/services/org.eclipse.microprofile.config.spi.Converter:

com.example.ColorConverter

Register programmatically

The builder implementation is internal (unexported package); obtain it through the resolver:

ConfigBuilder builder = ConfigProviderResolver.instance().getBuilder();
builder.withConverter(Color.class, 100, new ColorConverter());
Config config = builder.build();

Converter priority

Registered converters compete by priority — the highest priority wins, and on ties the last registered converter overrides the previous one:

  1. Built-in converters — priority 1

  2. Converters discovered via ServiceLoader — their @jakarta.annotation.Priority value, or 100 when the annotation is absent (the standard application priority)

  3. Converters from ConfigBuilder.withConverter(type, priority, converter) — the explicit priority argument (no special rank: 100 is the conventional value here too)

When no registered converter exists for the requested type, Ravel derives one: arrays are converted component-wise, and other types go through the implicit-converter fallback. If nothing applies, IllegalArgumentException is thrown.

Example: JSON converter

public class JsonConverter<T> implements Converter<T> {

    private final ObjectMapper mapper;
    private final Class<T> targetType;

    public JsonConverter(Class<T> type) {
        this.targetType = type;
        this.mapper = new ObjectMapper(); // or your serialization library
    }

    @Override
    public T convert(String value) {
        try {
            return mapper.readValue(value, targetType);
        } catch (Exception e) {
            throw new IllegalArgumentException("Invalid JSON for " + targetType.getName(), e);
        }
    }
}

// Usage
MyConfigClass config = ConfigProvider.getConfig()
    .getValue("app.config.json", MyConfigClass.class);

Next