Skip to content

Default Values and Comments

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

Default Values & Comments

Self-seeding defaults

getOrSetValueIfAbsent is the startup-safe way to introduce a setting: it writes the default only when the path is absent, and returns the existing value otherwise.

int port = cfg.getOrSetValueIfAbsent("server.port", 25565);              // value only
int max  = cfg.getOrSetValueIfAbsent("server.max-players", 20, "cap");   // value + comment

List<String> tags = cfg.getOrSetValueIfAbsent("tags", Arrays.asList("a", "b")); // list overload
  • When the path is absent, the default is written, the dirty flag and newDefaultValueToSave are set, and the default is returned.
  • When the path is present, the stored value is returned, recast to the default's runtime type — so a value stored as a long reads back as the Integer your default implies, with no ClassCastException.
boolean seeded = cfg.isNewDefaultValueToSave();   // did any default get written this run?
cfg.clearNewDefaultValueToSave();
cfg.setValueIfAbsent("server.motd", "Welcome!");  // thin wrapper over getOrSetValueIfAbsent

A key that contains a dot is escaped with a backslash. The path separator is always ., so to address a single key whose name has a dot, escape it as a\.b. cfg.getInt("rates.usd\\.brl") reads the one key "usd.brl" under rates (not rates → usd → brl); \\ is a literal backslash. The escape is a no-op for an ordinary key, so plain dotted paths are unaffected. There is no swappable separator.

Comments — two write modes

A comment can be rewritten on every save (documentation in code stays current) or written only once (a user's edit wins). This applies to both the fluent API and the @Comment annotation.

Fluent

cfg.setComment("server.port", "the listen port");          // AUTHORITATIVE — overwrites every save
cfg.setDefaultComment("server.port", "tune me");           // SET-IF-ABSENT — kept if one already exists
String c = cfg.getComment("server.port");                  // BLOCK comment by default
cfg.getComment("server.port", CommentType.SIDE);           // the side comment

getOrSetValueIfAbsent(path, def, comment) uses the set-if-absent rule for the comment.

A scalar list can carry a per-element block comment, addressed by the dotted index — setComment("tags.0", …) comments the first element, "tags.2" the third:

cfg.setComment("tags.0", "the primary tag");

Per-element list comments round-trip on YAML only — JSON, TOML and JSONC drop them. Object/nested list elements are not addressed this way. See Codecs & Formats.

Annotation

@Comment(value = "Database settings", mode = CommentMode.SET_IF_ABSENT)   // class -> file header
class DbConfig {
    @Comment("The JDBC url")                                              // OVERRIDE (default)
    String jdbcUrl = "jdbc:h2:mem:test";

    @Comment(value = "tune me", mode = CommentMode.SET_IF_ABSENT)         // user edit wins
    int maxPool = 10;
}
  • @Comment defaults to CommentMode.OVERRIDE — the comment is re-seeded on every binding save, so fixing the text in code reaches existing files.
  • @Comment(mode = SET_IF_ABSENT) writes only when the path has no comment yet.
  • A class-level @Comment becomes the file header (at the root). Use SET_IF_ABSENT on the class to keep a header the user wrote.

Comment writes are always safe: on a NONE-fidelity codec (JSON) the comment is simply not emitted, and the data is never corrupted. See Codecs & Formats.

File header & footer

The comment block at the very top (above the first key) and bottom (below the last) are first-class on the Config façade, with the same two write modes as field comments:

cfg.setHeader("=== My Plugin ===", "Do not edit while the server runs");  // OVERRIDE
cfg.setDefaultHeader("generated by EveryConfig");                         // only if no header yet
cfg.setFooter("end of file");                                            // the footer counterpart
List<String> header = cfg.getHeader();   // empty when none; clearHeader() removes it
  • Each argument may contain \n (split into lines), so a multi-line ASCII-art banner goes in as one argument.
  • The header never swallows the first key's own comment: a blank line separates them (an empty header line is emitted as a bare #///, so only the separator is truly blank).
  • A class-level @Comment on a bound POJO seeds the header too (its mode decides override vs set-if-absent); setHeader/setDefaultHeader follow the same precedence.

Like field comments, header/footer are held in memory but never written on a NONE-fidelity codec (JSON).

Moving a key — migrateKey

migrateKey is the explicit rename hook (reconciliation never infers a rename itself). It moves the data, the key's own block/side comment and its blank-lines-before, and marks the destination persisted so a later seed won't overwrite the migrated comment.

import br.com.finalcraft.everyconfig.config.MigrationResult;

// rename the old top-level "mysql" section to "database" — safe to run on every startup
MigrationResult r = cfg.migrateKey("mysql", "database");
if (r == MigrationResult.SOURCE_ABSENT) log.warn("nothing to migrate — typo in the source path?");

migrateKey returns a MigrationResult so a re-run or a typo is observable (the tree looks the same whether the source was already moved or never existed):

Result Meaning
MOVED the source moved to the destination (r.moved() is true). If the destination already held data, the source overwrote it.
SAME_PATH oldPath equals newPath — nothing to do.
INVALID_ROOT either path is the root, which cannot be migrated.
ALREADY_MIGRATED the source is gone but the destination exists — a benign re-run from an earlier startup.
SOURCE_ABSENT neither side exists — nothing was migrated, often a typo in oldPath.

It moves the whole comment subtree — the key's own comment and those of every descendant path — so a nested section keeps all of its documentation across the rename.

→ See also The Dynamic API · Annotations

Clone this wiki locally