-
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 merge a POJO back into the tree when you want to write — but the tree stays the source of truth.
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);loadAs/readIdCollection 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.readIdCollectionResult("servers", Server.class, codec);The same BindResult comes off an EntityBinder via binder.readResult(""). The old stateful getters
(EntityBinder.lastLoadIssues(), Config.lastIdCollectionIssues()) still work unchanged.
db.maxPool = 25;
cfg.setValue("", db); // a POJO setValue MERGES into the tree (root here), then cfg.save()A binding save merges, it never replaces:
- The tree wins on conflict / unknown keys survive. A key a user added by hand that the POJO doesn't declare stays in the file.
- The POJO value wins for declared keys. Your in-memory change is written.
- Comments declared via @Comment are seeded during the merge.
cfg.setValue("legacy.extra", "kept");
cfg.setValue("", new DbConfig());
cfg.contains("legacy.extra"); // still true — unknown keys survive the mergeBindOptions 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<Cfg> b = cfg.bind(Cfg.class, codec);
Cfg c = b.read("");
b.
lastLoadIssues(); // e.g. ['port' = 'NaN' (expected int): ...]
// STRICT: the first mismatch throws BindException
cfg.
bind(Cfg .class, codec, BindOptions.defaults().
withCoercion(BindOptions.Coercion.STRICT)).
bind();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 · @Id 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