Skip to content
Petrus Pradella edited this page Jul 1, 2026 · 6 revisions

FAQ & Gotchas

My comments disappeared after a save!

You probably saved with the JSON codec, whose comment fidelity is NONE — JSON has no comment syntax, so comments are accepted in memory but not emitted. Use YAML, TOML or JSONC if you need comments on disk. See Codecs & Formats.

A hand-edit to the file got overwritten

Outside a watcher, a Config lives in memory and save() never reads the file first — it dumps memory. Call reload() (or re-open()) before saving if you want a manual edit to survive. See Lifecycle, Reload & Watching.

TOML lost my null value

TOML has no null type. The TomlCodec omits a null-valued key on write, so it reads back as absent (the typed getter returns its default). If you need an explicit null on disk, use YAML, JSON or JSONC.

A huge integer came back as a string in TOML

The TOML reader mis-parses integers beyond the 64-bit range (and some near it). To keep the value intact, the TomlCodec writes such an integer as a quoted string — the numeric getters still read it as digits (getLong/getString both work), and on YAML/JSON it stays a number. This only affects values around 1e18 and larger.

Binding zeroed my field instead of keeping its default

It won't — lenient bind (the default) keeps the field's real default: the bad node is removed from a working copy of the tree and the bind is retried. Check binder.lastLoadIssues() to see what was skipped, or use STRICT to fail fast. See Entity Binding.

A key the user added by hand disappeared after a binding write

It shouldn't — a binding save (via EntityBinder.write) merges: the tree wins and unknown keys survive. If you explicitly set ObsoletePolicy.REMOVE, that strips keys the schema no longer declares — that's opt-in and destructive. The default is PRESERVE.

My hand-added keys disappeared after cfg.setValue(path, pojo)

That's expected — setValue(path, pojo) replaces the subtree at path: only the keys the POJO declares remain, so a key (and its comment) the POJO doesn't declare is dropped. To keep unknown keys, use mergeValue(path, pojo) — it merges the POJO into the subtree, and the tree wins for keys the POJO doesn't declare. (Sibling keys outside path are untouched either way.)

cfg.setValue("legacy.extra", "kept");
cfg.mergeValue("", new DbConfig());   // unknown keys survive; setValue("", ...) would drop legacy.extra

How do I keep an obsolete key but mark it deprecated?

Bind with ObsoletePolicy.COMMENT_OUT — it keeps the key (the data survives, as with PRESERVE) but stamps an authoritative deprecation block comment on it, overwriting any stale comment it carried.

BindOptions opts = BindOptions.defaults()
        .withObsoletePolicy(BindOptions.ObsoletePolicy.COMMENT_OUT);

LOSSLESS codecs only. On a codec that can't round-trip a comment losslessly (JSON = NONE, JSONC = LOSSY) it degrades to PRESERVE: the key is kept, but no marker is written.

@Section on a nested POJO field didn't move

It should — @Section relocates a field of a nested POJO too, with the path taken relative to that POJO's own location. The one unsupported spot is a @Section field inside a List/Map element (a collection element has no stable path). A sectioned type also honors ObsoletePolicy.REMOVE like any other. See Annotations.

Which getter for a value stored as a quoted number?

Any numeric getter — they tolerate a number stored as a string: getLong("1700000000000"), getInt("25565"), getDouble("3.14") all parse. An empty string reads as the default.

getInt returned the default on a present-but-null key

That's the trichotomy: an explicit null is present (contains is true) but a typed getter flattens it to the default. Use getValue/getNode to tell a real value from null. See The Dynamic API.

getString on a section gave me the default instead of the JSON

By design. getString on an object node returns the default (an object is not a string in any useful sense — same rule as the numeric getters returning their default on a type mismatch). An array still joins its elements with newlines (handy for a lines: block), and a scalar reads as its canonical text.

cfg.getString("database");                 // database is a section -> null (or your default)
cfg.getString("messages.kick");            // an array -> "line 1\nline 2"
cfg.getString("server.host", "0.0.0.0");   // a scalar -> "127.0.0.1"

Does getUUID throw on a malformed value?

No — both overloads are tolerant, like the numeric getters. A malformed or absent value yields null (getUUID(path)) or your default (getUUID(path, def)); neither ever throws.

UUID owner = cfg.getUUID("world.owner");                 // absent/garbage -> null
UUID owner = cfg.getUUID("world.owner", CONSOLE_UUID);   // ...or your fallback

I have a config key with a dot in its name

The path separator is fixed at ., but you can escape a dot that belongs to the key itself with a backslash — a\.b addresses the single key "a.b". \\ is a literal backslash, and the escaping is a no-op for an ordinary path.

// the tree has  rates: { "usd.brl": 5.12 }
double r = cfg.getDouble("rates.usd\\.brl");   // reads the single key "usd.brl" under "rates"

There is no swappable separatorPathOptions only exposes '.'. Escaping is the mechanism for a dotted key.

I called migrateKey and want to know if it actually moved anything

migrateKey(old, new) is safe to run unconditionally on every startup, and it returns a MigrationResult so a typo or a benign re-run is observable:

Result Meaning
MOVED the source was moved to the destination (if the destination already held a value, the source overwrites it)
SAME_PATH old and new are the same path — nothing to do
INVALID_ROOT either side is the root, which cannot be migrated
ALREADY_MIGRATED source gone, destination present — the migration ran on an earlier startup (benign)
SOURCE_ABSENT neither side exists — nothing migrated, often a typo in old (worth logging)
if (cfg.migrateKey("mysql.host", "database.host") == MigrationResult.SOURCE_ABSENT) {
    log.warn("nothing at mysql.host to migrate — check the path");
}

Can I use the typed/POJO API without a file?

Yes — Config.inMemory() gives the full typed/POJO API (setValue/mergeValue, getValue(path, type), @Key/@Section/@Comment, enum-by-name, java.time, Optional) with no file behind it, so save() throws. (A bare new Config() has no codec at all and accepts only native scalar/Map/list/JsonNode values.) To persist, open a real file instead. See The Dynamic API.

How do I write a config out in a different format?

Two hooks, both on a Config opened over a file:

  • save(Codec) — a one-shot persist through another codec; the codec the Config keeps is unchanged.
  • changeCodec(Codec) — switch the format every later save() uses.
Config cfg = Config.open("server.yml");   // YAML on disk
cfg.save(new JsonCodec());                  // one-shot JSON snapshot; live codec stays YAML
cfg.changeCodec(new TomlCodec());           // every future save() now emits TOML

The file extension is not rewritten, so emitting a format the name does not imply is your call — a later extension-inferred Config.open would pick the wrong codec.

Exotic YAML comments got mangled

The YAML comment parser is line-based and covers common block-style YAML. A # inside a |/> block scalar, anchors/merge-keys, explicit ? : keys, or multi-line flow can mis-attach a comment. The data is unaffected; only the comment overlay for those exotic shapes is best-effort.

Does Config.open guess the file format?

No — it never sniffs content. The single-arg overloads derive the codec from the file-name extension via CodecRegistry.defaults().forFile(...) and fail fast: a missing or unregistered extension throws a CodecException. Pass a codec explicitly to override or to use an unconventional name:

Config c = Config.open("settings.yml");                  // codec from ".yml"
Config c = Config.open(Paths.get("settings.cfg"), new YamlCodec()); // explicit

See Lifecycle, Reload & Watching.

Do I need to shade Jackson?

Not in the library — EveryConfig ships thin. A Bukkit/Spigot plugin that bundles it should relocate com.fasterxml.jackson and org.yaml.snakeyaml in its own shade step. See Installation.

Clone this wiki locally