Skip to content

Dynamic API

Petrus Pradella edited this page Jun 28, 2026 · 5 revisions

The 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.

Set & remove

cfg.setValue("a.b.c", 42);          // auto-vivifies intermediate objects (only objects, never arrays)
cfg.setValue("a.b.c", null);        // a Java null DELETES the entry
cfg.setValue("", someObjectNode);   // replaces the whole root (must be an object)
cfg.setValue("note", "hi", "a comment");   // value + comment in one call

boolean removed = cfg.removeValue("a.b");  // returns true if it existed; drops the subtree's comments

cfg.migrateKey("old.path", "new.path");    // move data + its full comment subtree to a new path

Replacing a container subtree drops the comments of its descendants (they no longer apply).

migrateKey is the explicit rename hook for a config migration (reconciliation never infers a rename). It carries the key's own block/side comment AND those of every descendant path, written as authoritative so they are preserved (not re-seeded). A root path, an equal path, or an absent source is a no-op.

Typed getters

Each getter has a zero-default form and a (path, default) form:

cfg.getString("name");          cfg.getString("name", "fallback");
cfg.getInt("port");             cfg.getInt("port", 25565);
cfg.getLong("ts");              cfg.getLong("ts", 0L);
cfg.getDouble("ratio");         cfg.getDouble("ratio", 1.0);
cfg.getBoolean("pvp");          cfg.getBoolean("pvp", true);
cfg.getStringList("tags");      cfg.getStringList("tags", defaultList);
cfg.getList("items", Integer.class);  // typed; empty (never null) when absent / not a list
cfg.getValue("db", Db.class);   // a subtree bound to a type (POJO or scalar)
cfg.getUUID("id");              cfg.getUUID("id", fallbackUuid);
  • getString on a list joins elements with \n; on an object it returns toString().
  • getStringList(path) returns an empty list when absent; getStringList(path, def) returns def only 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.

The absent / null / value trichotomy

These three states are distinct:

cfg.setValue("present", 5);
cfg.getRoot().putNull("explicitNull");   // present, but null

cfg.contains("missing");        // false   — absent
cfg.getValue("missing");        // null
cfg.getInt("missing", 9);       // 9

cfg.contains("explicitNull");   // true    — present but null
cfg.getValue("explicitNull");   // null
cfg.getInt("explicitNull", 9);  // 9       (a null flattens to the default)

cfg.contains("present");        // true    — a real value
cfg.getInt("present");          // 5

Keys, containment & sections

cfg.contains("a.b");                 // does the path resolve?
cfg.getKeys();                       // direct child keys of the root
cfg.getKeys("server");               // direct child keys of "server"
cfg.getKeys("", true);              // DEEP — dotted descendant paths

cfg.getConfigSection("a");           // always a view; use cfg.contains("a") for existence
cfg.getConfigSection("a");           // ALWAYS a (possibly empty) view

A 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"

Path-scoped typed read

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);

cfg.getValue("missing", DbConfig.class);  // absent path → the type's defaults
cfg.getValue("", Root.class);             // root path ("" or null) → the whole tree

The 2-arg form throws IllegalStateException if the config was built without a codec (new Config()). The full binding model (@Key, @Comment, @Section, lenient vs. strict, LoadIssues) lives in Entity Binding.

The escape hatch

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("rawKey", "set outside the API");

→ See also Default Values & Comments · Entity Binding

Clone this wiki locally