Skip to content

Rubric Builder API

Oliver Yasuna edited this page Jul 1, 2026 · 2 revisions

Rubric — Builder API

Not every config fits the annotated-POJO shape. When you need to define entries at runtime — from data files, a plugin registry, a scripted DSL — reach for ConfigSpec instead.

ConfigSpec is Rubric's programmatic model. It produces a Schema backed by a flat Map<String, Object> (dotted paths) rather than a POJO. Everything downstream — validators, migrations, sync, snapshots, the GUI — works identically. Only the definition style differs.

When to use it

Prefer ConfigSpec when:

  • The set of entries is only known at runtime (loaded from a data pack, from an addon registry, etc.).
  • You're bridging an existing configuration format that isn't Java-first.
  • You want dozens of similar entries generated in a loop.

Prefer annotations when the shape is fixed at compile time — it's terser, IDE-friendlier, and Rubric knows more about your intent (real field types, real defaults from field initializers).

Basic usage

import com.oliveryasuna.mc.rubric.api.Format;
import com.oliveryasuna.mc.rubric.schema.api.ConfigSpec;
import com.oliveryasuna.mc.rubric.schema.MapConfigModel;
import com.oliveryasuna.mc.rubric.schema.Schema;

// Build the spec: id + name + format + version + entries.
final ConfigSpec spec = ConfigSpec.builder("mymod", "config", Format.TOML, 1)
        .define("enabled", true, b -> b.comment("Master toggle for the demo feature."))
        .define("tickIntervalMillis", 200, b -> b
                .comment("Tick interval — server-authoritative.")
                .range(50, 5_000)
                .syncScope(Sync.Scope.SERVER))
        .define("quality", "medium", b -> b
                .comment("Quality preset.")
                .oneOf("low", "medium", "high"))
        .build();

// Wrap into a schema-backed model.
final MapConfigModel model = new MapConfigModel(spec);

// Hand to a ConfigManager just like an annotated model.
final ConfigManager<Map<String, Object>> manager = new ConfigManager<>(
        model,
        RubricSerialization.defaultIO(),
        Loaders.platform(),
        new CodecRegistry(),
        null);
manager.load();

Reading values

final Map<String, Object> state = manager.get();
final int tickInterval = (Integer)state.get("tickIntervalMillis");

Since the state is a Map<String, Object>, you cast. A ConfigValue<V> typed view still works — dotted paths address the same entries.

Mixing with annotations

You can register annotation-defined configs and builder-defined configs in the same runtime. Each is its own ConfigManager, each writes to its own file, and both use the same event bus, sync service, and GUI plumbing.

Limitations

  • The GUI shows entries with their dotted-key labels (e.g. display.opacity), which read uglier than field-name labels. There's a follow-up to humanize these — see YACL improvements & gaps and Cloth improvements & gaps.
  • No nested categories from the builder in the current API — everything is flat under spec.root(). Use dotted keys ("display.opacity") to group visually; the reader interprets them as nested when needed.
  • Custom validators, migrations, sync scopes, reload tiers, and widget overrides are all available on the builder — the builder chain mirrors the annotation set.

What next

  • Concepts for the vocabulary the builder chains produce.
  • Validation for wiring .range(...), .oneOf(...), and custom validators.
  • Migration — builder-defined configs migrate just like annotated ones.

Clone this wiki locally