-
Notifications
You must be signed in to change notification settings - Fork 0
Dynamic API
The dynamic API operates directly on the canonical ObjectNode using dot-separated paths. Typed getters route
through a single coercion seam (NodeCoercion), so "what does getInt do to a string node" is defined in one
place.
cfg.setValue("server.port", 25565); // auto-vivifies intermediate objects (only objects, never arrays)
cfg.setValue("server.motd", null); // a Java null DELETES the entry
cfg.setValue("", someObjectNode); // replaces the whole root (must be an object)
cfg.setValue("server.motd", "Welcome!", "Shown on the server list"); // value + comment in one call
boolean removed = cfg.removeValue("legacy.cache"); // true if it existed; drops the subtree's commentsReplacing a container subtree drops the comments of its descendants (they no longer apply).
The path separator is fixed at .. To address a key that legitimately contains a dot, escape it with a
backslash — a\.b is the single key "a.b", and \\ is a literal backslash. The escape is a no-op for
an ordinary key, so plain dotted paths are unaffected.
cfg.setValue("rates.usd\\.brl", 5.12); // the key "usd.brl" under "rates" — ONE level deep
cfg.getDouble("rates.usd\\.brl"); // 5.12
cfg.contains("rates.usd"); // false — there is no "usd" childThe separator is not swappable (
PathOptions(char)is private). Escaping is the only way to carry a dot inside a key.
migrateKey(old, new) is the explicit rename hook for a config migration (reconciliation never infers a
rename). It moves the data and the key's full comment subtree — the key's own block/side comment and
those of every descendant path — written as authoritative so they survive (not re-seeded).
It returns a MigrationResult so a re-run or a typo is observable, making it safe to call unconditionally
on every startup:
switch (cfg.migrateKey("database.poolSize", "database.pool.size")) {
case MOVED: break; // a real move happened (the only outcome that touched the tree)
case ALREADY_MIGRATED: break; // source gone, destination present — a benign re-run
case SOURCE_ABSENT: break; // neither side exists — often a typo in the old path
case SAME_PATH: break; // old == new
case INVALID_ROOT: break; // either side is the root path ("")
}When both paths already hold data, the source overwrites the destination and the result is
MOVED.result.moved()is a shortcut for "the tree changed this call".
Each getter has a zero-default form and a (path, default) form:
cfg.getString("server.motd"); cfg.getString("server.motd", "A Minecraft Server");
cfg.getInt("server.port"); cfg.getInt("server.port", 25565);
cfg.getLong("stats.firstSeen"); cfg.getLong("stats.firstSeen", 0L);
cfg.getDouble("economy.startBal"); cfg.getDouble("economy.startBal", 100.0);
cfg.getBoolean("world.pvp"); cfg.getBoolean("world.pvp", true);
cfg.getStringList("motd.lines"); cfg.getStringList("motd.lines", defaultLines);
cfg.getList("ports", Integer.class); // typed; empty (never null) when absent / not a list
cfg.getValue("database", Db.class); // a subtree bound to a type (POJO or scalar)
cfg.getUUID("owner.uuid"); cfg.getUUID("owner.uuid", fallbackUuid);-
getStringon a list joins elements with\n; on an object it returns the default (an object is not a string — like the numeric getters on a type mismatch, never the raw JSON). -
getUUID(path)andgetUUID(path, def)are both tolerant: an absent or malformed value yieldsnull/ the default and never throws. -
getStringList(path)returns an empty list when absent;getStringList(path, def)returnsdefonly when the path is absent.
Legacy long-as-string tolerance. The numeric getters parse a number stored as a quoted string:
getLong("1700000000000")→ the long,getLong("1.0")→1,getInt("25565")→25565. An empty string reads as the default.
These three states are distinct:
cfg.setValue("maxPlayers", 20);
cfg.getRoot().putNull("whitelist"); // present, but null
cfg.contains("difficulty"); // false — absent
cfg.getValue("difficulty"); // null
cfg.getInt("difficulty", 2); // 2
cfg.contains("whitelist"); // true — present but null
cfg.getValue("whitelist"); // null
cfg.getInt("whitelist", 9); // 9 (a null flattens to the default)
cfg.contains("maxPlayers"); // true — a real value
cfg.getInt("maxPlayers"); // 20cfg.contains("database.url"); // does the path resolve?
cfg.getKeys(); // direct child keys of the root
cfg.getKeys("database"); // direct child keys of "database"
cfg.getKeys("", true); // DEEP — dotted descendant paths
cfg.getConfigSection("database"); // ALWAYS a (possibly empty) view — use contains() for existenceA ConfigSection is a scoped cursor: every method delegates to the owning Config with the section's path
prefixed.
ConfigSection db = cfg.getConfigSection("database");
db.setValue("url", "jdbc:..."); // writes "database.url"
db.getInt("pool.size"); // reads "database.pool.size"
db.getSectionKey(); // "database"getValue(path, type) binds the subtree at a dotted path to a fresh POJO — the path-scoped form of
loadAs. The 2-arg form uses the codec the config was opened with; pass a Codec to override it.
DbConfig db = cfg.getValue("database", DbConfig.class); // bind the "database" subtree
DbConfig db = cfg.getValue("database", DbConfig.class, yamlCodec); // override the codec
cfg.getValue("database", DbConfig.class); // absent path → the type's defaults
cfg.getValue("", RootConfig.class); // root path ("" or null) → the whole treeThe 2-arg form throws
IllegalStateExceptionif the config was built without a codec (new Config()). The full binding model (@Key,@Comment,@Section, lenient vs. strict,LoadIssues) lives in Entity Binding.
A Config does not need a file. Config.inMemory() gives the full typed/POJO API (setValue/mergeValue,
getValue(path, type), @Key/@Section/@Comment, enum-by-name, java.time, Optional) backed only by
the in-memory tree — there is no text format, so save() throws. This differs from a bare new Config(),
which has no codec and accepts only native (scalar / Map / list / JsonNode) values.
Config cfg = Config.inMemory();
cfg.setValue("database", new DbConfig()); // POJO write works — no file needed
DbConfig db = cfg.getValue("database", DbConfig.class);A file-backed config can persist in, or switch to, another format:
Config cfg = Config.open(Paths.get("config.yml")); // codec inferred from the extension
cfg.save(new JsonCodec()); // one-shot dump as JSON to the SAME file; the live codec stays YAML
cfg.changeCodec(new TomlCodec()); // every subsequent save() now emits TOML
save(codec)andchangeCodec(codec)do not rename the file. Emitting a format the extension does not imply is your call — a later extension-inferredConfig.openwould pick the wrong codec. Switching from a comment-bearing codec to one without fidelity (e.g. JSON) drops the comment overlay on the next save.
getRoot() exposes the live ObjectNode — the single source of truth. Mutate it directly when you need raw
Jackson; the dynamic API and binding both see the change, and unknown keys you add survive every save.
cfg.getRoot().put("internalFlag", "set outside the API");→ See also Default Values & Comments · Entity Binding
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