Skip to main content

Data Types & Collections

MittenLib supports a wide range of Java types out of the box, from primitives to complex nested structures. This guide shows how to use them effectively in your config interfaces.

Interfaces vs. Classes

While MittenLib technically supports using classes (DTOs) for configuration, Interfaces are the recommended and primary way to define configurations, with classes being deprecated. This guide focuses exclusively on the Interface approach.

Primitive Types

All Java primitives and their wrapper types are supported.

@Config
public interface ServerConfig {
String name();
int port();
double tickRate();
boolean debug();
long timeout();
float multiplier();
}

Spigot Types

Common Spigot types (Location, ItemStack, etc.) are currently unsupported. There are a few reasons for this:

  • These types are often mutable by default and have complex internals, adding potential footguns.
  • These types don't always have clearly defined ways to serialize and deserialize -- for example, serializing a Location can often cause a StackOverflowError due to the World circular reference.

The recommended approach is to define a custom config type that represents the actual shape of the data you want to work with.

An alternative, but not recommended. option is to define a me.bristermitten.mittenlib.config.extension.CustomSerializer for these types.

Nullable Fields

By default, all fields are required and must be present in the config file. To allow null values, use the @Nullable annotation.

@Config
public interface DatabaseConfig {
String host();

@Nullable String tablePrefix(); // Can be null or omitted
}

Default Values

Provide defaults using Java default methods to make fields optional in the config file.

@Config
public interface ConnectionConfig {
String host();

default int port() {
return 3306;
}

default boolean ssl() {
return true;
}
}

Collections

Lists

Lists "just work" in the way you'd expect.

@Config
public interface Messages {
default List<String> motd() {
return List.of("Welcome!", "Have fun!");
}
}

Maps

Maps are represented as objects in YAML/JSON.

@Config
public interface Commands {
Map <String, String> descriptions();
}

Nested Configs

When a property's type is another @Config interface, the processor treats it as a nested object.

This means we can split config types into smaller subtypes, which can be reused in multiple places.

@Config
public interface PluginConfig {
String pluginName();
DatabaseConfig database(); // Nested config
}

@Config
public interface DatabaseConfig {
String host();
default int port() {
return 3306;
}
}

Enums

Java enums are automatically deserialized by name. By default, this is case-sensitive, but can be customised with the EnumParsingScheme annotation.

public enum GameMode {
SURVIVAL,
CREATIVE
}

@Config
public interface WorldSettings {
default GameMode defaultMode() {
return GameMode.SURVIVAL;
}
}

Inheritance

Interfaces can extend other interfaces to share fields across multiple configurations.

When we extend an interface, we essentially "absorb" all of its properties alongside any extra ones we define.

This can be extremely useful for defining "templates" which can be extended with properties.

tip

When using interfaces as configs, we can also use multiple-inheritance to extend multiple configs at once!

public interface BaseConfig {
String name();
}

@Config
public interface ExtendedConfig extends BaseConfig {
int extraField();
}

Union Types

There are often situations when a config property has a variety of different forms. For example, when defining the data storage system for our plugin, we might want to support JSON files and a database:

storage:
file: "file.json"
storage:
host: 127.0.0.1
port: 3306
# etc

We can use the ConfigUnion annotation to generate code for this.

First, define a config interface that acts as a root type for all the "variants". This should be marked with @ConfigUnion

We then define all the variants as nested configs, which must extend the root type.

The processor will generate code that tries to deserialize each variant in order, accepting the first success.

@Config
@ConfigUnion
public interface StorageConfig {

@Config
interface FileBasedStorageConfig extends StorageConfig {
String file();
}

@Config
interface DatabaseStorageConfig extends StorageConfig {
String host();
int port();
// etc
}

}
warning

The variants are tried in order, and so each variant needs to be distinct to avoid ambiguity. For example, in this example:

@Config
@ConfigUnion
public interface AmbiguousUnionConfig {
@Config
interface Option1 extends AmbiguousUnionConfig {
String blah();
}

@Config
interface Option2 extends AmbiguousUnionConfig {
String blah();
int other();
}
}

The Option2 variant will never be matched, because the Option1 variant will always succed first!

Next Steps

  • Naming & Keys - Customize how field names appear in config files
  • Validation - Add constraints to ensure data integrity