Skip to content

Validation and Lifecycle Hooks

GMalvestiti edited this page Aug 31, 2026 · 2 revisions

Implement ConfigExtension on the config class to participate in the load/save/validate lifecycle:

@Config(name = "mymod")
public final class MyModConfig implements ConfigExtension {

    public int hudScale = 2;
    public String worldPreset = "default";

    @Override
    public void afterLoad() {
        // Fix up values after loading — clamp ranges, derive computed fields, etc.
        hudScale = Math.max(1, Math.min(8, hudScale));
    }

    @Override
    public void beforeSave() {
        // Normalize values before writing to disk, if needed.
        // Most configs leave this empty.
    }

    @Override
    public void validate(List<Violation> violations) {
        // Report constraint violations. Must be side-effect free.
        if (hudScale < 1 || hudScale > 8) {
            violations.add(Violation.of("hud-scale.range", "hudScale must be between 1 and 8, was " + hudScale));
        }
    }
}

Hook order and contract:

  • afterLoad runs after deserialization and before validation. It does not run for updates, so an update mutator must submit values that already satisfy validation.
  • validate runs during holder construction, after each load, and before each update is published. It must be side-effect free: report invalid values rather than changing them.
  • beforeSave runs on a private copy immediately before serialization. Its changes reach the file without changing the published state.

Rules declared with @Range, @Pattern, and @Length are checked before validate and produce violations of the same kind, so a simple bound needs no hook at all. See Constraints and Metadata. Constraint annotations are checked against their field types when the holder is built; invalid combinations fail with INVALID_CONSTRAINT rather than being silently ignored during validation.

Handling validation failures:

Under FALLBACK update policy, violations come back on the UpdateResult:

UpdateResult result = holder.updateAndSave(config -> config.hudScale = 99);
if (!result.accepted()) {
    result.violations().forEach(v -> LOGGER.warn("{}: {}", v.id(), v.message()));
}

Under STRICT, the same violations ride on LiteConfigException.violations().

Validation on load: if the file on disk contains values that fail validate, the behavior depends on @Config.readFailurePolicy. Under FALLBACK, the file is backed up and defaults are restored. Holder creation writes those defaults immediately; a later load() keeps them in memory until the next save. Under STRICT, the load throws.

These hooks belong to the config model. To react after a holder completes a load, update, or save, see Lifecycle Listeners.

Clone this wiki locally