Skip to content

Self Describing Types

Petrus Pradella edited this page Jul 6, 2026 · 1 revision

Self-Describing Types

A custom type can carry its own config codec, so it round-trips with no central registration. Because EveryConfig is Jackson-first, the shared mapper discovers the type's codec and applies it in every context — a solo value, a POJO field, or a collection element.

Jackson-native (recommended)

A type that declares @JsonValue (write) + @JsonCreator (read) already self-describes — these are honored by the shared mapper unchanged.

class Coord {
    final int x, y;
    Coord(int x, int y) { this.x = x; this.y = y; }

    @JsonValue
    String encode() { return x + ":" + y; }              // -> the string "3:4"

    @JsonCreator
    static Coord of(String s) {
        String[] p = s.split(":");
        return new Coord(Integer.parseInt(p[0]), Integer.parseInt(p[1]));
    }
}

cfg.setValue("spawn", new Coord(3, 4));   // stored as "3:4" — solo, as a field, or one-per-line in a list
Coord back = cfg.getValue("spawn", Coord.class);

A type that serializes to an object works the same way — an immutable POJO rebuilt via a @JsonCreator constructor with @JsonProperty args, for example. Any Jackson mechanism (@JsonSerialize(using=) / @JsonDeserialize(using=), custom modules on your own mapper) applies too.

Enums

A plain enum always serializes by name() (stable and hand-editable). An enum that declares a @JsonValue keeps its custom form instead:

enum Transport {
    NIO("nio"), EPOLL("epoll");
    final String code;
    Transport(String code) { this.code = code; }

    @JsonValue String code()               { return code; }      // stored as "nio" / "epoll"
    @JsonCreator static Transport of(String c) { /* look up by code */ }
}

Marker interfaces (no annotations)

Prefer an explicit contract? Implement one of two interfaces (package br.com.finalcraft.everyconfig.selfdescribe). The write half is the interface method; the read half is a static factory found by convention (a Java interface cannot mandate a static method).

class Coord implements EveryConfigString<Coord> {           // a compact string form
    @Override public String toConfigString()       { return x + ":" + y; }
    public static Coord fromConfigString(String s)  { /* parse */ }   // convention: fromConfigString
}

class Endpoint implements EveryConfigMap<Endpoint> {        // a structured object form
    @Override public Map<String, Object> toConfigMap() {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("host", host); m.put("port", port);
        return m;
    }
    public static Endpoint fromConfigMap(Map<String, Object> m) {     // convention: fromConfigMap
        return new Endpoint((String) m.get("host"), ((Number) m.get("port")).intValue());
    }
}

A type implementing these interfaces but missing its convention factory fails fast on the first read. Prefer @JsonValue/@JsonCreator when you want the read contract compiler-checked; the interface trades that for a single self-typed marker.

An EveryConfigMap value is stored raw — its own form owns the shape, so it is not schema-merged as a bound entity and its keys carry no per-key comments.

Gotchas

A codec is required. Both forms need a codec-backed Config (Config.open / Config.inMemory). A bare new Config() has no mapper and takes only native values (scalars, Map, List, JsonNode).

Untyped reads see the raw form. getValue(path) without a type token returns the raw string/map — a self-describing codec reaches the typed reads (getValue(path, Type), getList(path, Type)) and binding, not the untyped dynamic read (there is no type to reconstruct from).

A different form inside a collection

Want a type that is rich as a solo value but compact inside a list? That is a distinct, opt-in mechanism — see the compact element form in @KeyIndex Collections.

→ See also Entity Binding · Annotations · The Dynamic API

Clone this wiki locally