-
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. A @JsonValue that returns a
Map<String, Object> (paired with a @JsonCreator that takes a Map) stores the value in a structured
form; an immutable POJO rebuilt via a @JsonCreator constructor with @JsonProperty args is another shape.
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 */ }
}Can't add annotations to the type (a third-party class)? Declare the same @JsonValue / @JsonCreator on a
Jackson mixin and register it on the codec's mapper — the mapper applies it as if the annotations were on
the class itself.
abstract class CoordMixin {
@JsonValue abstract String encode();
@JsonCreator static Coord of(String s) { return Coord.parse(s); }
}
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Coord.class, CoordMixin.class); // now Coord self-describes through this mapper
Config cfg = Config.open(path, new YamlCodec(mapper));A codec is required. A self-describing type needs a codec-backed
Config(Config.open/Config.inMemory) — the mapper is what applies@JsonValue/@JsonCreator. 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