Static Field Configs

Use pistonconfig-static-fields when config keys need to be shared across application code. Each property keeps its path, type, default value, and comment in one declaration.

Add the Module

dependencies {
  implementation(platform("net.pistonmaster:pistonconfig-bom:0.1.0-SNAPSHOT"))
  implementation("net.pistonmaster:pistonconfig-core")
  implementation("net.pistonmaster:pistonconfig-static-fields")
  implementation("net.pistonmaster:pistonconfig-yaml")
}

Declare Properties

final class ServerOptions implements StaticConfigComments {
  @ConfigComment("Address used by the public listener.")
  static final ConfigProperty<String> HOST =
    ConfigProperty.of("server.host", String.class, "0.0.0.0");

  @ConfigComment("Port used by the public listener.")
  static final ConfigProperty<Integer> PORT =
    ConfigProperty.of("server.port", Integer.class, 25565);

  static final ConfigProperty<List<String>> FLAGS = ConfigProperty.of(
    "server.flags",
    ConfigType.listOf(ConfigType.of(String.class)),
    List.of("default")
  ).withComment("Feature flags enabled at startup.");

  private ServerOptions() {
  }

  @Override
  public void registerComments(StaticConfigCommentRegistry comments) {
    comments.setRootComment("Server configuration.");
    comments.setComment("server", "Network listener settings.");
  }
}

ConfigProperty.of covers simple codec-backed types. Use ConfigType for parameterized values such as lists, sets, maps, optionals, and arrays.

Update a File

var store = StaticConfigStore.builder()
  .holders(ServerOptions.class)
  .format(YamlConfigFormat.INSTANCE)
  .options(StaticConfigStoreOptions.builder()
    .unknownKeyPolicy(StaticUnknownKeyPolicy.PRESERVE)
    .invalidValuePolicy(StaticInvalidValuePolicy.FALLBACK_AND_REWRITE)
    .build())
  .build();

var session = store.update(Path.of("config.yml"));

update creates the file when it is missing, runs configured document migrations, merges defaults, refreshes generated comments, rewrites invalid declared values when configured, saves the document, and returns a stateful session.

Read and Write Values

int port = session.get(ServerOptions.PORT);

session.set(ServerOptions.PORT, 25566);
session.save();
session.reload();

Use resolve when code needs to know whether a value came from the source document or from a fallback:

var value = session.resolve(ServerOptions.PORT);

if (value.requiresRewrite()) {
  logger.warn("Using default for {}", value.property().path());
}

Direct StaticConfigDefinition.get stays strict for present values. It returns the default only when the path is missing.

Use Parameterized Types

enum Mode {
  DEV,
  PROD
}

record Endpoint(String host, int port) {
}

static final ConfigProperty<Map<Mode, Endpoint>> ENDPOINTS = ConfigProperty.of(
  "server.endpoints",
  ConfigType.mapOf(ConfigType.of(Mode.class), ConfigType.of(Endpoint.class)),
  Map.of(Mode.DEV, new Endpoint("localhost", 8080))
).withComment("Endpoint per runtime mode.");

Register a codec for custom simple values:

var codecs = new ConfigCodecRegistry()
  .register(Endpoint.class, new ConfigCodec<Endpoint>() {
    @Override
    public ConfigNode encode(Endpoint value, ConfigCodecRegistry registry) {
      return ConfigNode.object()
        .set(ConfigPath.of("host"), value.host())
        .set(ConfigPath.of("port"), value.port());
    }

    @Override
    public Endpoint decode(ConfigNode node, ConfigCodecRegistry registry) {
      return new Endpoint(
        node.find(ConfigPath.of("host")).flatMap(ConfigNode::asString).orElseThrow(),
        node.find(ConfigPath.of("port")).flatMap(ConfigNode::asInt).orElseThrow()
      );
    }
  });

Then pass the registry to the store:

var store = StaticConfigStore.builder()
  .holders(ServerOptions.class)
  .format(YamlConfigFormat.INSTANCE)
  .codecRegistry(codecs)
  .build();

Validate Holder Classes

Use StaticConfigDefinitionValidator in unit tests:

@Test
void staticConfigDeclarationsAreValid() {
  new StaticConfigDefinitionValidator().validate(ServerOptions.class);
}

The default validation checks that property fields are static final, holder classes are final with one private no-args constructor, every property has a comment, comment lines fit the default length limit, enum comments list every enum constant, and defaults can encode and decode.

When to Choose Static Fields

Choose static fields when config keys are used in multiple services, commands, or libraries. Choose annotation configs when the application wants one typed config object as its startup boundary.

Search Documentation