Repository navigation
KeyIndex Collections
A collection of entities, each carrying a @KeyIndex field, is stored as a section keyed by the index value instead of a positional array. The index value becomes the key and is omitted from each entry's body; on read the section key is the sole authority for the index.
This is automatic: setValue detects that the element type carries @KeyIndex and writes the keyed
layout, and getList detects it (plus the stored node's shape) and reads it back. There are no dedicated
methods.
class Account {
@KeyIndex public String name;
public int balance;
public Account() {}
public Account(String name, int balance) { this.name = name; this.balance = balance; }
}
cfg.setValue("accounts", Arrays.asList(
new Account("alice", 100),
new Account("bob", 50))); // auto key-major: Account carries @KeyIndex
cfg.save();accounts:
alice:
balance: 100
bob:
balance: 50List<Account> back = cfg.getList("accounts", Account.class);
// back = [Account(alice, 100), Account(bob, 50)] — ids restored from the section keysA collection whose element type does not carry
@KeyIndexis stored as an ordinary array. On read,getListdiscriminates by the stored node's shape: an object section is read key-major, a plain array is read positionally — so a@KeyIndextype stored (legacy) as an array still binds, ids coming from the body.
A List, Set or array field whose element type carries @KeyIndex gets the same layout when the bean is
bound, at the root or nested, so a block of named entries lives in the settings bean and takes a @Comment:
class Settings {
@Comment("Each section is an account, named by its key.")
public List<Account> accounts = new ArrayList<>(Arrays.asList(
new Account("alice", 100), new Account("bob", 50)));
}
Settings settings = cfg.getOrMergeValue("", new Settings());# Each section is an account, named by its key.
accounts:
alice:
balance: 100
bob:
balance: 50- The file wins. Once the file has the section, its entries are the collection; a default element the file does not hold is not brought back.
-
The collection owns the membership. A binding write (
mergeValue,setValue,getOrMergeValue) merges each element into its own section - a key the entity does not declare survives inside it - and removes the section of an element that left the collection. -
An empty collection is written as
[]. -
A section that does not bind (a stale
accounts.enabled: [...], a key the id type cannot take) is oneLoadIssueataccounts.<key>and is skipped; one bad value inside an entry is an issue ataccounts.<key>.<field>and only that field keeps its default.Coercion.STRICTthrows instead.getOrMergeValuewrites neither back: the unbindable section and the bad value stay in the file as typed. -
Element hooks receive a section at
owner.accounts.<id>.
A file written before this layout still reads. A field stored as a plain list of objects binds as it always did, ids coming from the body. The first binding write after that turns it into the keyed section, so the next
save()changes the file's layout. A section the read skipped is dropped by that write as well: the bean could not hold it, and the collection owns the membership.
The layout follows the field's declared element type. A
List<Object>or a rawListthat happens to hold@KeyIndexentities stays a plain list, and so does a list nested inside aMaphanded tosetValue.
- Stable identity. Entries are addressed by their index value, not position — reordering the file or hand-editing one entry doesn't disturb the others.
-
Readable & diffable.
accounts.alice.balanceis a real path you can read with the dynamic API. -
The key wins. On read, the section key is the index value; a stray
nameinside a body is ignored.
@KeyIndex may be String, a boxed/primitive numeric (int, long, …), boolean, or UUID. The value is
rendered as the key token and cast back on read.
class World { @KeyIndex public UUID id; public String name; }
cfg.setValue("worlds", Arrays.asList(new World(uuid, "overworld")));
List<World> worlds = cfg.getList("worlds", World.class); // uuid restored from the key- A duplicate index value in the collection (two entries with the same key), or a type with two
@KeyIndexfields or an unsupported@KeyIndextype, is rejected with aBindExceptiononsetValue. - On a lenient read, a per-entry problem (e.g. a body index value disagreeing with its section key, which the
key wins) is recorded as a
LoadIssue. The plaingetListdiscards those issues;getListResultreturns them with the list.
BindResult<List<Account>> result = cfg.getListResult("accounts", Account.class);
List<Account> back = result.value();
if (result.hasIssues()) {
for (LoadIssue issue : result.issues()) { /* report or log */ }
}
result.issues()is an unmodifiable snapshot taken for that read.
@KeyIndex gives a collection a keyed layout. A sibling feature gives a type a compact form inside a list
while its solo form stays rich — so a long List<Pos> is a tidy string-list, not a wall of {x, y, z}
objects.
Declare the two annotations (package br.com.finalcraft.everyconfig.annotation): @EveryConfigCompactValue
on the instance method that returns the compact one-line String, and @EveryConfigCompactCreator on a
public static factory (or a constructor) that takes a String. Jackson ignores both, so the solo/field
form stays rich. Then — exactly like @KeyIndex — setValue and getList auto-detect the type for a
collection; there are no dedicated methods.
class Pos {
public int x, y, z;
public Pos() {}
public Pos(int x, int y, int z) { this.x = x; this.y = y; this.z = z; }
@EveryConfigCompactValue
public String toLine() { return x + " " + y + " " + z; }
@EveryConfigCompactCreator
public static Pos fromLine(String s) {
String[] p = s.trim().split("\\s+");
return new Pos(Integer.parseInt(p[0]), Integer.parseInt(p[1]), Integer.parseInt(p[2]));
}
}
cfg.setValue("home", new Pos(1, 2, 3)); // solo -> a rich object { x, y, z }
cfg.setValue("spots", spots); // a List<Pos> -> ["4 5 6", "7 8 9"] (auto-compact)
List<Pos> back = cfg.getList("spots", Pos.class);getList is tolerant for such a type: a textual element is rebuilt via the @EveryConfigCompactCreator,
an object element via the normal rich bind (so a list mixing an old rich object and a new compact string both
read), and a malformed element is skipped leniently.
Auto-detection is type-driven, like
@KeyIndex: declaring the annotations makes everyList<Pos>compact via the dynamic API (there is no per-call opt-out). A@KeyIndextype keeps its keyed layout — that check wins.
The compact form is the dynamic API only — a nested
List<T>field inside a bound POJO stays rich.
For a type you can't add the annotations to, attach a CompactElementResolver to the codec (every jackson
codec has the constructor):
CompactElementResolver resolver = type ->
type == Pos.class ? new PosCompactCodec() : null; // CompactElementCodec<Pos>: encode(Pos)->String, decode(String)->Pos
Config cfg = Config.open(path, new YamlCodec(mapper, resolver));Both types live in br.com.finalcraft.everyconfig.selfdescribe. This is a per-codec seam — there is no
global registry. EveryConfig ships AnnotationCompactElementResolver (it reads the two annotations); every
jackson codec uses it by default and composes your resolver ahead of it.
→ See also Entity Binding · Annotations · Self-Describing Types
EveryConfig · br.com.finalcraft.everyconfig:everyconfig-core · One config API, every format, comments included · made by Petrus Pradella
Getting Started
Core Concepts
Typed Binding
Operations
Reference
Contributing