-
Notifications
You must be signed in to change notification settings - Fork 21
Advanced Topics
This guide covers advanced features for extending okaeri-configs with custom serialization logic and transformers.
- Overview
- Custom Serializers
- Custom Transformers
- ConfigSerializable Interface
- Creating Serdes Packs
- SerdesRegistry Operations
Okaeri Configs provides multiple extension points for handling custom types:
| Extension Type | Use Case | Complexity |
|---|---|---|
| ConfigSerializable | Class-local serdes, auto-registered | Low |
| ObjectTransformer | Simple type conversions (A → B, faster) | Low |
| ObjectSerializer | Multi-field types, custom logic | Medium |
| OkaeriSerdesPack | Grouping multiple serializers/transformers | Medium |
| ConfigPostprocessor | Custom file format manipulation | High |
Use ObjectSerializer for types that need custom serialization logic.
Use serializers for:
- Multi-field types (Location with x, y, z, world, yaw, pitch)
- Platform-specific types (Bukkit ItemStack, PotionEffect)
- Complex types requiring custom logic
- Simple types (can use
setValue()for single-value serialization)
Prefer transformers for simple conversions:
- Transformers use hashed matching (faster lookup)
- Serializers use iterative matching (slower for many types)
- For basic type conversions (String ↔ Integer, String ↔ Locale), transformers are more efficient
Prefer Serializable/subconfigs for:
- Your own POJOs → use
Serializableor subconfigs
Implement ObjectSerializer<T>:
import eu.okaeri.configs.schema.GenericsDeclaration;
import eu.okaeri.configs.serdes.*;
import lombok.NonNull;
public class CoordinateSerializer implements ObjectSerializer<Coordinate> {
@Override
public boolean supports(@NonNull Class<?> type) {
return Coordinate.class.isAssignableFrom(type);
}
@Override
public void serialize(@NonNull Coordinate object, @NonNull SerializationData data, @NonNull GenericsDeclaration generics) {
data.add("x", object.getX());
data.add("y", object.getY());
data.add("z", object.getZ());
}
@Override
public Coordinate deserialize(@NonNull DeserializationData data, @NonNull GenericsDeclaration generics) {
double x = data.get("x", Double.class);
double y = data.get("y", Double.class);
double z = data.get("z", Double.class);
return new Coordinate(x, y, z);
}
}MyConfig config = ConfigManager.create(MyConfig.class, (it) -> {
it.configure(opt -> {
opt.configurer(new YamlSnakeYamlConfigurer(), registry -> {
registry.register(new CoordinateSerializer());
});
opt.bindFile("config.yml");
});
it.load(); // load only, don't save after
});Alternative (if you already have a configurer instance):
MyConfig config = new MyConfig();
config.configure(opt -> {
opt.configurer(new YamlSnakeYamlConfigurer());
opt.serdesPack(registry -> {
registry.register(new CoordinateSerializer());
});
opt.bindFile("config.yml");
});
config.load();| Method | Description | Example |
|---|---|---|
add(key, value) |
Add simple value | data.add("x", 10.5) |
add(key, value, type) |
Add typed value | data.add("world", world, World.class) |
setValue(value) |
Set single value (no key) | data.setValue("value") |
addCollection(key, list, type) |
Add collection | data.addCollection("coords", list, Coordinate.class) |
| Method | Description | Example |
|---|---|---|
get(key, type) |
Get typed value | data.get("x", Double.class) |
getValue(type) |
Get single value | data.getValue(String.class) |
getAsList(key, type) |
Get typed list | data.getAsList("coords", Coordinate.class) |
containsKey(key) |
Check if key exists | data.containsKey("optional") |
Real-world example from serdes-bukkit:
public class LocationSerializer implements ObjectSerializer<Location> {
@Override
public boolean supports(@NonNull Class<?> type) {
return Location.class.isAssignableFrom(type);
}
@Override
public void serialize(@NonNull Location location, @NonNull SerializationData data, @NonNull GenericsDeclaration generics) {
data.add("world", location.getWorld(), World.class);
data.add("x", location.getX());
data.add("y", location.getY());
data.add("z", location.getZ());
data.add("yaw", location.getYaw());
data.add("pitch", location.getPitch());
}
@Override
public Location deserialize(@NonNull DeserializationData data, @NonNull GenericsDeclaration generics) {
World world = data.get("world", World.class);
double x = data.get("x", Double.class);
double y = data.get("y", Double.class);
double z = data.get("z", Double.class);
float yaw = data.get("yaw", Float.class);
float pitch = data.get("pitch", Float.class);
return new Location(world, x, y, z, yaw, pitch);
}
}Output (YAML):
location:
world: world
x: 100.5
y: 64.0
z: -50.25
yaw: 90.0
pitch: 0.0Use ObjectTransformer for simple type conversions (A → B) with no intermediate structure.
Use transformers for:
- Basic type conversions (String ↔ Integer, String ↔ Enum)
- String ↔ Locale, UUID, Pattern
- Custom ↔ String conversions for simple types
Why transformers for simple types?
- Performance: Transformers use hashed matching (fast lookup by type pair)
- Simplicity: Direct A → B conversion without intermediate data structures
- Serializers use iterative matching (slower when many serializers registered)
When to use serializers instead:
- Multi-field types → use
ObjectSerializer - Complex serialization logic → use
ObjectSerializer
Implement ObjectTransformer<From, To>:
import eu.okaeri.configs.schema.GenericsPair;
import eu.okaeri.configs.serdes.*;
import lombok.NonNull;
import java.util.Locale;
public class LocaleTransformer extends ObjectTransformer<String, Locale> {
@Override
public GenericsPair<String, Locale> getPair() {
return this.genericsPair(String.class, Locale.class);
}
@Override
public Locale transform(@NonNull String data, @NonNull SerdesContext context) {
return Locale.forLanguageTag(data.replace("_", "-"));
}
}For two-way conversions, use BidirectionalTransformer:
import eu.okaeri.configs.serdes.BidirectionalTransformer;
public class ColorTransformer extends BidirectionalTransformer<String, Color> {
@Override
public GenericsPair<String, Color> getPair() {
return this.genericsPair(String.class, Color.class);
}
@Override
public Color leftToRight(@NonNull String data, @NonNull SerdesContext context) {
// String → Color
return Color.decode(data);
}
@Override
public String rightToLeft(@NonNull Color data, @NonNull SerdesContext context) {
// Color → String
return String.format("#%06X", data.getRGB() & 0xFFFFFF);
}
}MyConfig config = ConfigManager.create(MyConfig.class, (it) -> {
it.configure(opt -> {
opt.configurer(new YamlSnakeYamlConfigurer(), registry -> {
// One-way transformer
registry.register(new LocaleTransformer());
// Bidirectional transformer (registers both directions)
registry.register(new ColorTransformer());
});
opt.bindFile("config.yml");
});
it.load(); // load only, don't save after
});For transformers that serialize via .toString():
config.configure(opt -> {
opt.serdesPack(registry -> {
registry.registerWithReversedToString(new StringToIntegerTransformer());
// Registers: String → Integer AND Integer → String (via toString)
});
});Use ConfigSerializable for class-local serdes - serialization logic lives in the class itself.
Advantages:
- No external serializer needed
- Self-contained serialization logic
- Works automatically (no registration)
Use for:
- Application-specific types you control
- Types where serialization is part of the domain logic
Don't use for:
- External library types (can't modify source)
- Types needing multiple serialization formats
Implement the interface and provide a static deserialize method:
import eu.okaeri.configs.serdes.serializable.ConfigSerializable;
import eu.okaeri.configs.schema.GenericsDeclaration;
import eu.okaeri.configs.serdes.*;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player implements ConfigSerializable {
private String name;
private int level;
private double experience;
@Override
public void serialize(@NonNull SerializationData data, @NonNull GenericsDeclaration generics) {
data.add("name", this.name);
data.add("level", this.level);
data.add("experience", this.experience);
}
public static Player deserialize(@NonNull DeserializationData data, @NonNull GenericsDeclaration generics) {
String name = data.get("name", String.class);
int level = data.get("level", Integer.class);
double experience = data.get("experience", Double.class);
return new Player(name, level, experience);
}
}@Getter
@Setter
public class GameConfig extends OkaeriConfig {
private Player player = new Player(); // Works automatically!
private List<Player> topPlayers = List.of(
new Player("Alice", 50, 1250.0),
new Player("Bob", 45, 980.5)
);
}Output (YAML):
player:
name: ''
level: 0
experience: 0.0
topPlayers:
- name: Alice
level: 50
experience: 1250.0
- name: Bob
level: 45
experience: 980.5ConfigSerializable objects can contain other ConfigSerializable objects:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Guild implements ConfigSerializable {
private String name;
private Player leader; // Nested ConfigSerializable
@Override
public void serialize(@NonNull SerializationData data, @NonNull GenericsDeclaration generics) {
data.add("name", this.name);
data.add("leader", this.leader, Player.class);
}
public static Guild deserialize(@NonNull DeserializationData data, @NonNull GenericsDeclaration generics) {
String name = data.get("name", String.class);
Player leader = data.get("leader", Player.class);
return new Guild(name, leader);
}
}Required deserialize signature:
public static YourType deserialize(@NonNull DeserializationData data, @NonNull GenericsDeclaration generics)Common mistakes:
// ❌ WRONG - not static
public YourType deserialize(...)
// ❌ WRONG - wrong return type
public static Object deserialize(...)
// ❌ WRONG - missing parameters
public static YourType deserialize(DeserializationData data)Use OkaeriSerdesPack to group multiple serializers and transformers into a reusable module.
Use for:
- Platform-specific type bundles (Bukkit, Bungee)
- Library integrations (Bucket4j, Adventure)
- Organizational purposes (group related serializers)
Implement OkaeriSerdesPack:
import eu.okaeri.configs.serdes.*;
import lombok.NonNull;
public class MyCustomSerdes implements OkaeriSerdesPack {
@Override
public void register(@NonNull SerdesRegistry registry) {
// Register transformers
registry.register(new LocaleTransformer());
registry.register(new ColorTransformer());
// Register serializers
registry.register(new CoordinateSerializer());
registry.register(new PlayerSerializer());
}
}MyConfig config = ConfigManager.create(MyConfig.class, (it) -> {
it.configure(opt -> {
opt.configurer(
new YamlSnakeYamlConfigurer(),
new SerdesCommons(), // Built-in pack
new MyCustomSerdes() // Your custom pack
);
opt.bindFile("config.yml");
});
it.load(); // load only, don't save after
});From the library source:
public class SerdesCommons implements OkaeriSerdesPack {
@Override
public void register(@NonNull SerdesRegistry registry) {
registry.register(new DurationTransformer());
registry.register(new DurationAttachmentResolver());
registry.register(new InstantSerializer(false));
registry.register(new LocaleTransformer());
registry.register(new PatternTransformer());
}
}Preferred approach (using configure):
config.configure(opt -> {
opt.configurer(new YamlSnakeYamlConfigurer(), registry -> {
// Standard registration (last wins)
registry.register(new MySerializer());
// Register first (high priority)
registry.registerFirst(new HighPrioritySerializer());
// Exclusive registration (removes others for this type)
registry.registerExclusive(MyType.class, new ExclusiveSerializer());
});
});Direct access (when needed):
SerdesRegistry registry = config.getConfigurer().getRegistry();
registry.register(new MySerializer());// Get serializer for type
ObjectSerializer<?> serializer = registry.getSerializer(MyType.class);
// Check if transformation is possible
boolean canTransform = registry.canTransform(
GenericsDeclaration.of(String.class),
GenericsDeclaration.of(Integer.class)
);
// Get transformer
ObjectTransformer transformer = registry.getTransformer(
GenericsDeclaration.of(String.class),
GenericsDeclaration.of(Locale.class)
);Last registered wins:
registry.register(new MySerializer1()); // ← Will be checked second
registry.register(new MySerializer2()); // ← Will be checked firstFirst registration (highest priority):
registry.registerFirst(new HighPrioritySerializer()); // ← Checked first
registry.register(new NormalSerializer()); // ← Checked secondExclusive registration:
// Remove all serializers for ItemStack, register only this one
registry.registerExclusive(ItemStack.class, new CustomItemStackSerializer());-
Use the right abstraction:
// ✅ Simple conversion new StringToLocaleTransformer() // ✅ Complex multi-field type new LocationSerializer() // ✅ Your own POJOs implements ConfigSerializable
-
Group related serializers:
public class MyGameSerdes implements OkaeriSerdesPack { // All game-related serializers in one place }
-
Check for null/optional values:
@Override public void serialize(Location loc, SerializationData data, GenericsDeclaration generics) { if (loc.getWorld() != null) { data.add("world", loc.getWorld(), World.class); } }
-
Prefer transformers for basic type conversions (performance):
// ⚠️ Works but slower (iterative matching) public class LocaleSerializer implements ObjectSerializer<Locale> { public void serialize(Locale locale, SerializationData data, GenericsDeclaration generics) { data.setValue(locale.toLanguageTag()); } public Locale deserialize(DeserializationData data, GenericsDeclaration generics) { return Locale.forLanguageTag(data.getValue(String.class)); } } // ✅ Better - faster (hashed matching, shown earlier in guide) public class LocaleTransformer extends ObjectTransformer<String, Locale>
-
Don't forget the static deserialize method:
// ❌ Will fail at runtime public class Player implements ConfigSerializable { public void serialize(...) { } // Missing: public static Player deserialize(...) }
-
Don't register serdes after config is created:
// ❌ Too late - config already initialized MyConfig config = ConfigManager.create(...); config.configure(opt -> { opt.serdesPack(registry -> { registry.register(new MySerializer()); }); }); // ✅ Register during config creation MyConfig config = ConfigManager.create(MyConfig.class, (it) -> { it.configure(opt -> { opt.configurer(new YamlSnakeYamlConfigurer(), registry -> { registry.register(new MySerializer()); }); opt.bindFile("config.yml"); }); it.load(); // load only, don't save after });
- Serdes Extensions - Pre-built serializers for common types
- Subconfigs & Serialization - Understanding serialization basics
- Examples & Recipes - Real-world examples
- Configuration Basics - Understanding core concepts
- Validation - Adding validation to custom types