Skip to content

Versioning and Migrations

GMalvestiti edited this page Aug 31, 2026 · 1 revision

When a config class changes shape — a field is renamed, moved into a section, or split in two — older files still use the previous keys. Migrations transform those files before binding, preserving the values that remain meaningful in the new model.

Declaring a version

Versioning is opt-in. A config that leaves version alone behaves exactly as before, and its file gains nothing.

@Config(name = "mymod", version = 3)
public final class MyModConfig {
    public HudSettings hud = new HudSettings();
    public String profile = "default";
}

A versioned file stores its revision under the reserved configVersion key, written first so it is the first thing a reader sees:

{
  "configVersion": 3,
  "hud": { "scale": 2 },
  "profile": "default"
}

The key is reserved: a field that would claim it is rejected with RESERVED_VERSION_KEY when the holder is built.

Declaring the steps

One step per version, written as a static method on the config class itself, next to the fields it upgrades. Each step moves the file forward by exactly one revision, and Lite Config runs the steps it needs in order.

@Config(name = "mymod", version = 3)
public final class MyModConfig {
    public HudSettings hud = new HudSettings();
    public String profile = "default";

    @Migration(from = 1)
    static void toVersion2(ConfigData data) {
        data.rename("hudScale", "hud.scale");
    }

    @Migration(from = 2)
    static void toVersion3(ConfigData data) {
        data.set("profile", "default");
    }
}

A file at version 1 runs both steps; a file at version 2 runs only the second; a file already at version 3 runs none. The result is stamped with the declared version and saved.

A file that carries no configVersion key is treated as version 1, because that is what it was before you started versioning.

When present, configVersion must be a positive whole number in the Java int range. null, strings, booleans, objects, arrays, fractions, zero, negatives, and out-of-range numbers are malformed config data. Only a completely absent key receives the version 1 compatibility default.

Writing a step

A step receives a ConfigData view of the parsed file. It works in dotted paths and plain Java types, so a migration never depends on the file format or on the JSON library underneath.

@Migration(from = 1)
static void toVersion2(ConfigData data) {
    data.rename("hudScale", "hud.scale")
        .set("volume", data.integer("volume", 1) * 10)
        .remove("legacyFlag");
}

A step must be static, return void, and take a single ConfigData. A declaration that breaks any of those rules, or a from value that two steps claim at once, is reported as INVALID_MIGRATION when the holder is built.

The mutating calls return the same view, so a step usually reads as a short list of edits. Reads come in two shapes: the Optional ones tell you whether the file had a value at all, and the ones taking a fallback answer with a plain value, which is what a migration usually wants.

Method Purpose
has(path) whether the path is present
string, number, bool read a value, empty when absent or of another kind
string(path, fallback), integer, decimal, bool read a value, falling back when the file has nothing usable
set(path, value) write a value, creating the objects along the way
remove(path) delete a value, doing nothing when it is absent
rename(from, to) relocate a value, doing nothing when the source is absent

Steps run against the raw file, before it is bound to your class, so a step can freely reference fields that no longer exist in Java.

When something goes wrong

Situation Error
The file's version is above the declared one CONFIG_VERSION_TOO_NEW
No step is registered for a version the file has to cross MISSING_CONFIG_MIGRATION
A step throws MIGRATION_FAILED
A step is malformed, or two steps claim the same version INVALID_MIGRATION

CONFIG_VERSION_TOO_NEW means a player downgraded the mod, which is a runtime situation and follows the read failure policy: FALLBACK moves the newer file to a .corrupt-... backup before saving defaults, preserving all original data; STRICT throws and leaves the file in place. The other three are mistakes in the mod itself and always propagate, whatever the policy. Every exception thrown by a migration, including a LiteConfigException, is reported as MIGRATION_FAILED with its root cause preserved.

Clone this wiki locally