-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
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 */ }
}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/@JsonCreatorwhen you want the read contract compiler-checked; the interface trades that for a single self-typed marker.
An
EveryConfigMapvalue 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.
A codec is required. Both forms need a codec-backed
Config(Config.open/Config.inMemory). A barenew 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).
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
EveryConfig · br.com.finalcraft:EveryConfig · One config API, every format, comments included · made by Petrus Pradella
Getting Started
Core Concepts
Typed Binding
Operations
Reference
Contributing