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.
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.
- Java
- YAML
@Config
public interface ServerConfig {
String name();
int port();
double tickRate();
boolean debug();
long timeout();
float multiplier();
}
name: "My Server"
port: 25565
tickRate: 20.0
debug: false
timeout: 5000
multiplier: 1.5
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
Locationcan often cause aStackOverflowErrordue to theWorldcircular 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.
- Java
- YAML
@Config
public interface DatabaseConfig {
String host();
@Nullable String tablePrefix(); // Can be null or omitted
}
host: "localhost"
# tablePrefix can be omitted or set to null
tablePrefix: null
Default Values
Provide defaults using Java default methods to make fields optional in the config file.
- Java
- YAML
@Config
public interface ConnectionConfig {
String host();
default int port() {
return 3306;
}
default boolean ssl() {
return true;
}
}
host: "localhost"
# port and ssl will use their default values if omitted
Collections
Lists
Lists "just work" in the way you'd expect.
- Java
- YAML
@Config
public interface Messages {
default List<String> motd() {
return List.of("Welcome!", "Have fun!");
}
}
motd:
- "Welcome!"
- "Have fun!"
Maps
Maps are represented as objects in YAML/JSON.
- Java
- YAML
@Config
public interface Commands {
Map <String, String> descriptions();
}
descriptions:
help: "Show help message"
spawn: "Teleport to spawn"
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.
- Java
- YAML
@Config
public interface PluginConfig {
String pluginName();
DatabaseConfig database(); // Nested config
}
@Config
public interface DatabaseConfig {
String host();
default int port() {
return 3306;
}
}
pluginName: "MyPlugin"
database:
host: "localhost"
port: 3306
Enums
Java enums are automatically deserialized by name. By default, this is case-sensitive, but can be customised with the EnumParsingScheme annotation.
- Java
- YAML
public enum GameMode {
SURVIVAL,
CREATIVE
}
@Config
public interface WorldSettings {
default GameMode defaultMode() {
return GameMode.SURVIVAL;
}
}
defaultMode: CREATIVE # or "creative" if case-sensitivity disabled
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.
When using interfaces as configs, we can also use multiple-inheritance to extend multiple configs at once!
- Java
- YAML
public interface BaseConfig {
String name();
}
@Config
public interface ExtendedConfig extends BaseConfig {
int extraField();
}
name: "Base Name"
extraField: 100
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.
- Java
- YAML (Option 1)
- YAML (Option 2)
@Config
@ConfigUnion
public interface StorageConfig {
@Config
interface FileBasedStorageConfig extends StorageConfig {
String file();
}
@Config
interface DatabaseStorageConfig extends StorageConfig {
String host();
int port();
// etc
}
}
storage:
file: "file.json"
storage:
host: 127.0.0.1
port: 3306
# etc
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