This guide covers advanced features and edge cases in YamlAnnotations.
Leniency controls how strictly type conversions are enforced during YAML loading.
| Level | Description |
|---|---|
STRICT |
Exact type matching required; throws exceptions on mismatches |
LENIENT |
Attempts automatic type coercion when possible |
UNDEFINED |
Field-level only: inherits from @YamlFile; defaults to LENIENT when @YamlFile is absent |
Class-level (applies to all fields):
@YamlFile(lenient = Leniency.LENIENT)
public class Config extends YamlFileInterface {
// All fields use LENIENT mode by default
}Field-level (overrides class setting):
@YamlFile(lenient = Leniency.STRICT)
public class Config extends YamlFileInterface {
@YamlKey("strict-value")
public int strictValue = 0; // Uses STRICT
@YamlKey(value = "lenient-value", lenient = Leniency.LENIENT)
public char lenientValue = 'A'; // Uses LENIENT
}char-field: "Hello"| Mode | Result |
|---|---|
STRICT |
Throws IOException - string too long |
LENIENT |
Takes first character: 'H' |
float-field: 0.123456789| Mode | Result |
|---|---|
STRICT |
Throws IOException - precision loss |
LENIENT |
Converts with precision loss: 0.12345679f |
list-field: "single-item"| Mode | Result |
|---|---|
STRICT |
Throws IOException - expected list |
LENIENT |
Wraps in list: ["single-item"] |
Any class with a public constructor that takes a single String parameter can be used:
public class Email {
private final String address;
public Email(String address) {
if (!address.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
this.address = address;
}
@Override
public String toString() {
return address;
}
}
public class Config extends YamlFileInterface {
@YamlKey("contact-email")
public Email contactEmail = new Email("admin@example.com");
}contact-email: admin@example.comFor types with public static fields (common in game APIs):
// Given a class like:
public class Sound {
public static final Sound CLICK = new Sound("click");
public static final Sound DING = new Sound("ding");
// ...
}
// You can use it directly:
public class Config extends YamlFileInterface {
@YamlKey("notification-sound")
public Sound notificationSound = Sound.DING;
}notification-sound: DINGThe library will:
- On save: Find the static field name that matches the value
- On load: Look up the static field by name
Configuration classes can extend other configuration classes.
Important: Loading traverses the full class hierarchy, but saving only writes fields declared in the concrete class. Fields inherited from a parent class are not written on save.
public abstract class BaseConfig extends YamlFileInterface {
@YamlKey("version")
public int version = 1;
@YamlKey("debug")
public boolean debug = false;
}
public class PluginConfig extends BaseConfig {
@YamlKey("plugin.name")
public String pluginName = "MyPlugin";
@YamlKey("plugin.enabled")
public boolean enabled = true;
}Saving new PluginConfig() writes only the fields declared in PluginConfig:
plugin:
name: MyPlugin
enabled: trueLoading will populate version and debug from the YAML file if those keys are present (since load traverses all parent classes), but they will not appear in freshly saved files.
The library automatically handles Bukkit's Keyed interface (like Sound, Material, etc.):
public class Config extends YamlFileInterface {
@YamlKey("break-sound")
public Sound breakSound = Sound.BLOCK_STONE_BREAK;
@YamlKey("item-type")
public Material itemType = Material.DIAMOND_SWORD;
}break-sound: BLOCK_STONE_BREAK
item-type: DIAMOND_SWORDUse the plugin instance for automatic path resolution:
@YamlFile(fileName = "settings.yml")
public class Settings extends YamlFileInterface {
@YamlKey("setting")
public String setting = "value";
}
// In your plugin:
public class MyPlugin extends JavaPlugin {
private Settings settings;
@Override
public void onEnable() {
// Automatically uses: plugins/MyPlugin/settings.yml
settings = new Settings().load(this);
}
@Override
public void onDisable() {
settings.save(this);
}
}| Exception | Cause |
|---|---|
IOException |
File read/write errors, type conversion failures |
FinalAttribute |
Attempting to use @YamlKey on a final field |
DuplicateKey |
Same key path used multiple times |
If a YAML file doesn't contain a key, the field keeps its default value:
public class Config extends YamlFileInterface {
@YamlKey("existing")
public String existing = "default"; // Loaded from YAML if present
@YamlKey("missing")
public String missing = "default"; // Keeps "default" if not in YAML
}Primitive types cannot be null. This will throw an exception:
# Error: Cannot assign null to primitive
int-field: nullUse wrapper types if you need nullable values:
@YamlKey("nullable-int")
public Integer nullableInt = null; // Works fineload()reads the entire file into memorysave()writes the entire file atomically- For large configurations, consider splitting into multiple files
The library uses reflection for field access on every load() and save() call. For performance-critical applications:
- Minimize the number of annotated fields
- Load configuration once at startup
- Cache the configuration instance
List→ArrayList(preserves insertion order)Set→LinkedHashSet(preserves insertion order)Queue→ArrayDequeMap→LinkedHashMap(preserves insertion order)
YamlAnnotations is not thread-safe. If you need concurrent access:
public class ThreadSafeConfig {
private final Config config;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public ThreadSafeConfig(String path) throws IOException {
config = new Config().load(path);
}
public String getValue() {
lock.readLock().lock();
try {
return config.value;
} finally {
lock.readLock().unlock();
}
}
public void setValue(String value) {
lock.writeLock().lock();
try {
config.value = value;
} finally {
lock.writeLock().unlock();
}
}
}Before (Bukkit):
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
String name = config.getString("player.name", "Unknown");
int level = config.getInt("player.level", 1);After (YamlAnnotations):
public class Config extends YamlFileInterface {
@YamlKey("player.name")
public String name = "Unknown";
@YamlKey("player.level")
public int level = 1;
}
Config config = new Config().load(file);
// Access directly: config.name, config.level- Type safety at compile time
- IDE auto-completion
- Refactoring support
- Default values in one place
- Self-documenting configuration structure