Skip to content

Default Values and Comments

Petrus Pradella edited this page Aug 2, 2026 · 6 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 and JSONC — JSON and TOML 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).

Vertical spacing — blank lines above an entry

A file where every comment is glued to the entry above it is hard to read. Two independent mechanisms open it up; both are rendering decisions, and neither is stored in the file as a setting.

The policy — a config-wide floor

cfg.withBlankLineBeforeComments(1);        // one blank line above every COMMENTED root key
cfg.withBlankLineBeforeComments(1, 2);     // ...reaching down to depth 2 (a top-level section's keys)
cfg.withCommentStyle(CommentStyle.of(2, 1));   // the same thing, spelled out
CommentStyle style = cfg.getCommentStyle();

It is a floor, applied at emit time, and it is deliberately narrow:

It floats a blank line above… It never touches…
an entry that carries a block comment an entry with no comment
an entry within maxDepth (1 = root keys) anything deeper than maxDepth
an entry that is not the first emitted in its block the first entry of the document, a section or a [table] — the header or the parent key already separates it
a list element, the file header, or the footer
spacing the file already had that is wider (a floor never tightens)

Like a key-order pin this is an in-memory policy: re-assert it at startup, it survives reload(), and nothing is written to mark it. It is off by default, so a plain load/save is byte-identical.

The directive — blank lines at one path

Empty lines opening a comment are a spacing request, not comment text. They become blank lines above that entry, at any depth, whether or not the policy is on:

cfg.setComment("DebugMode", "\n\nDebug system.");        // two blank lines above DebugMode
cfg.setBlankLinesBefore("DebugMode", 3);                  // the same thing, without touching the text
int n = cfg.getBlankLinesBefore("DebugMode");             // what is DECLARED for this path
cfg.setBlankLinesBefore("DebugMode", 0);                  // and this is how you take them away

The three authoring forms below are identical — the annotation flattens its lines with \n:

@Comment("\n\nEnabled debug modules.")
@Comment({"", "", "Enabled debug modules."})
cfg.setComment("DebugMode.modules", "\n\nEnabled debug modules.");

Where both apply, the wider one wins: max(directive, policy).

The rules that keep existing files intact

  • An empty line in the middle of a comment is unchanged — it still emits a bare #/// marker.

  • A comment made only of empty lines is not a directive; setComment(p, "") still emits one bare marker.

  • setHeader/setFooter are never directives.

  • Writing a comment without a directive does not touch spacing — so a @Comment OVERRIDE reseed keeps the separation a user typed into the file. With a directive, the author owns it and rewrites it every save.

  • Blank lines that came from the file are never re-read as a directive, so a "cushioned" block round-trips as written:

    #
    # Section two
    #
    key: 1
  • A list element keeps its own spacing (setBlankLinesBefore("tags.1", 1)), and no policy floors it.

The ratchet. Once a save has emitted those blank lines they are part of the file, so a later load reads them as that path's own spacing. Turning the policy off, or lowering it, does not take them back — setBlankLinesBefore(path, 0) does. A plain-text file carries no record of who put a blank line there.

Per codec: YAML, JSONC and TOML honor the policy; JSON has no structure emitter and is unaffected. In TOML "the first entry of a block" follows the emitted order — bare key = value pairs come before sub-sections, so a table's first entry is its first scalar.

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