-
Notifications
You must be signed in to change notification settings - Fork 0
Entity Binding
Binding is a derived view over the canonical tree. You read the tree into a POJO when you want types, and
write a POJO back into the tree when you want to persist — but the tree stays the source of truth. A write can
either replace the target subtree (setValue) or merge into it (mergeValue); see the write section.
DbConfig db = cfg.loadAs(DbConfig.class, codec); // bind the whole tree + run @PostLoadloadAs binds the whole tree to a fresh instance and runs @PostLoad. For control over
options, get an EntityBinder:
EntityBinder<DbConfig> binder = cfg.bind(DbConfig.class, codec, options);
DbConfig db = binder.read("");
List<LoadIssue> issues = binder.lastLoadIssues();getValue(path, type) is the path-scoped loadAs — it binds the subtree at a dotted path to a
fresh POJO. An absent path binds the type's defaults; a root path ("" or null) binds the whole tree.
PoolConfig pool = cfg.getValue("database.pool", PoolConfig.class); // bind just database.poolThe 2-arg form uses the codec the config was opened with (it throws IllegalStateException if the config
was opened without one, e.g. new Config()); pass a codec explicitly to override:
PoolConfig pool = cfg.getValue("database.pool", PoolConfig.class, codec);Binding needs a codec — even with no file. The codec supplies the mapper that drives the POJO ⇄ tree conversion.
Config.open(...)carries one; a barenew Config()does not (and accepts only native values). For a file-less config that still wants the full typed/POJO API, useConfig.inMemory()— it binds an in-memory codec, sogetValue(path, type), a POJOsetValue/mergeValue,@Key/@Section/@Comment, enums-by-name andjava.timeall work; onlysave()throws (there is no file). See The Dynamic API.
loadAs/getList discard the issues collected for the call. The *Result variants return a
BindResult<T> — the value and the issue list (an unmodifiable snapshot) travel together, so a later
bind can't overwrite them out from under you:
import br.com.finalcraft.everyconfig.binding.BindResult;
BindResult<DbConfig> r = cfg.loadAsResult(DbConfig.class, codec);
DbConfig db = r.value();
if (r.hasIssues()) log.warn("{} bind issue(s): {}", r.issues().size(), r.issues());
BindResult<List<Server>> servers = cfg.getListResult("servers", Server.class);The same BindResult comes off an EntityBinder via binder.readResult(""), and the stateful
EntityBinder.lastLoadIssues() still works unchanged.
Both project the POJO annotation-aware — @Key/@Section/@Comment honored, enum-by-name, java.time,
Optional, and @Comment seeded — and both leave sibling keys outside the target path
untouched. They differ in what happens to the subtree at the path:
db.maxPool = 25;
cfg.setValue("", db); // OVERRIDE: the subtree at the path becomes exactly what the POJO declares
cfg.mergeValue("", db); // MERGE: the POJO wins for its keys; unknown keys under the path survive-
setValue(path, pojo)REPLACES the subtree atpath. Only the keys the POJO declares remain; a key (and its comment) that the POJO does not declare does not survive. Sibling keys outsidepathare untouched. -
mergeValue(path, pojo)MERGES into the subtree atpath. The POJO is the source of truth only for the keys it declares; a key a user added by hand that the POJO doesn't declare stays (the tree wins). The POJO value wins for declared keys, and its@Comments are seeded (never over a user-edited comment). AmergeValue(path, pojo, comment)overload seeds the path's own comment when absent.
cfg.setValue("legacy.extra", "kept");
cfg.mergeValue("", new DbConfig());
cfg.contains("legacy.extra"); // still true — mergeValue keeps unknown keys (setValue would drop them)The typed binder still merges.
mergeValueis the façade over the binder's merge; the underlyingEntityBinder.writehas always merged and still does (see the next sections).setValueis the override façade — it projects the POJO the same way, then replaces the node at the path.
BindOptions controls how a value that can't be coerced to its field type is handled.
import br.com.finalcraft.everyconfig.binding.BindOptions;
// LENIENT (default): a bad value is recorded as a LoadIssue and the field keeps its real default
EntityBinder<DbConfig> b = cfg.bind(DbConfig.class, codec);
DbConfig db = b.read("");
b.lastLoadIssues(); // e.g. ['port' = 'NaN' (expected int): ...]
// STRICT: the first mismatch throws BindException
cfg.bind(DbConfig.class, codec, BindOptions.defaults()
.withCoercion(BindOptions.Coercion.STRICT))
.read("");A LoadIssue carries the key, the rawValue, the targetType and a message. A @PostLoad method may
take a List<LoadIssue> to inspect them and decide whether to reject the config.
Lenient keeps the real default. The bad node is removed from a working copy of the tree and the bind is retried, so the field keeps the POJO's actual default value — not a zeroed placeholder.
BindOptions.ObsoletePolicy decides what happens to a file key the bound schema no longer declares:
-
PRESERVE(default) — keep it untouched (the tree always wins). -
REMOVE— strip it from the tree during the merge (opt-in; destroys data the schema dropped). -
COMMENT_OUT— keep the key (its data survives, likePRESERVE) but stamp an authoritative deprecation block comment on it, overwriting any stale comment it carried.
cfg.bind(Cfg.class, codec, BindOptions.defaults().withObsoletePolicy(BindOptions.ObsoletePolicy.REMOVE));
COMMENT_OUTis LOSSLESS-codecs only. On a codec that can't round-trip a comment losslessly (aNONE/LOSSYcodec, e.g. JSON/JSONC) it degrades toPRESERVE— the key is kept, no marker written.
@Section relocates a field under a dotted path. It applies to a field of a nested POJO too — there the path is relative to that POJO's own location, not the document root:
class Server {
@Section("network") int port = 25565; // under <server-location>.network.port
}A
@Sectionfield inside aList/Mapelement is unsupported (a collection element has no stable path). A sectioned type now honorsObsoletePolicy.REMOVE— its obsolete keys are stripped like any other, no longer forced toPRESERVE.
Because the mapper is plain Jackson, native annotations work alongside EveryConfig's:
@JsonProperty, @JsonIgnore, @JsonAlias, @JsonTypeInfo/@JsonSubTypes (polymorphism), etc.
class Holder {
@JsonProperty("max-pool") int maxPool = 10;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({@JsonSubTypes.Type(value = Circle.class, name = "circle")})
Shape shape;
}Polymorphic round-trips work and the type tag survives a merge — including under
ObsoletePolicy.REMOVE: the@JsonTypeInfodiscriminator is treated as a declared key, not an obsolete one, so it is never stripped.
→ See also Annotations · @KeyIndex Collections
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