Skip to content
Pétrus Pradella edited this page Oct 23, 2026 · 8 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. getOrMergeValue also leaves the bad text in the file instead of writing the default over it. 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 separator — PathOptions 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");
}

How do I keep a Debug section always at the bottom of the file?

New keys are appended at the end on save, so a section you want last slowly rises as you seed more keys over time. Pin it:

cfg.pinLast("Debug");     // "Debug" sinks below its siblings on every save, whatever gets seeded later

The pin is an in-memory policy, not written to the file — call it again each startup (the same way you re-run @Comment / getOrSetValueIfAbsent). pinFirst floats a key to the top, unpin clears it, and it works at any depth (pinLast("a.b") sinks b inside a).

Best-effort per format: YAML/JSONC honor it fully; TOML keeps a pinned section last but cannot move a scalar past a table; JSON's plain output keeps live-tree order (no structure emitter). See The Dynamic API.

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