Skip to content

HOCON serializers

Addon developer

AbstractMenus uses its own wrappers over the Lightbend HOCON config library. A serializer is a small factory that takes a ConfigNode and returns a Java object.

Each serializer implements NodeSerializer<T>. The interface has one method: deserialize(Class<T> type, ConfigNode node).

You usually don’t need to register a serializer manually. The five register(...) calls on the type registries (action, rule, item-property, activator, catalog) accept a NodeSerializer<S> and wire it into the shared NodeSerializers collection automatically.

The case where you do register a serializer manually: when you want to deserialize a custom parameter object used by your action or rule, so HOCON parsing can read it nested inside another structure via node.getValue(MyType.class).

api.serializers().register(MyType.class, new MyTypeSerializer());

Do this from MenuExtension.onEnable(api). By the time onEnable returns for your addon, AbstractMenus is still pre-menu-load, so any types you register are available when menus parse.

AbstractMenus ships serializers for Java primitives and a few common types out of the box:

  • Boolean
  • Integer
  • Long
  • Float
  • Double
  • String
  • UUID

Plus its own value types: TypeBool, TypeInt, TypeDouble, TypeString, TypeMaterial, TypeLocation, TypeSlot, etc.

public class User {
public String name;
public int age;
}
user {
name: "Notch"
age: 42
}
public class UserSerializer implements NodeSerializer<User> {
@Override
public User deserialize(Class<User> type, ConfigNode node) throws NodeSerializeException {
User user = new User();
user.name = node.node("name").getString();
user.age = node.node("age").getInt();
return user;
}
}

ConfigNode is the parsed structure. The serializer reads named fields off node and copies them into the target type.

If a field of your type is itself a serializable type, call getValue(SomeType.class) and let the registered serializer for SomeType do the work:

user {
name: "Notch"
age: 42
friend {
name: "Alex"
age: 38
}
}
public class User {
public String name;
public int age;
public User friend;
}
public class UserSerializer implements NodeSerializer<User> {
@Override
public User deserialize(Class<User> type, ConfigNode node) throws NodeSerializeException {
User user = new User();
user.name = node.node("name").getString();
user.age = node.node("age").getInt();
user.friend = node.node("friend").getValue(User.class);
return user;
}
}

getValue(User.class) looks up the registered UserSerializer and runs it on the nested node. Make sure UserSerializer is registered before any HOCON that references it parses, otherwise getValue throws.

HOCON supports lists. The API supports lists of any registered type.

user {
name: "Notch"
age: 42
friends: [
{ name: "Petya", age: 34 },
{ name: "Alex", age: 38 }
]
}
public class User {
public String name;
public int age;
public List<User> friends;
}
public class UserSerializer implements NodeSerializer<User> {
@Override
public User deserialize(Class<User> type, ConfigNode node) throws NodeSerializeException {
User user = new User();
user.name = node.node("name").getString();
user.age = node.node("age").getInt();
user.friends = node.node("friends").getList(User.class);
return user;
}
}

getList(SomeType.class) works for any type that has a registered serializer, including primitives. node.getList(String.class) returns a List<String>.

ConfigNode exposes the common reads you’d expect:

node.getString() // primitive string
node.getString("default") // primitive string with default
node.getInt()
node.getInt(0)
node.getBoolean()
node.getDouble()
node.getList(String.class)
node.isNull()
node.isPrimitive()
node.isMap()
node.isList()
node.node("path") // child node by dotted path (one or many segments)
node.child("name") // single-step child lookup
node.childrenList() // List<ConfigNode> for list nodes
node.childrenMap() // Map<String, ConfigNode> for map nodes
node.hasChildren()
node.key() // name of this node in its parent
node.path() // full dotted path from root
node.parent() // parent ConfigNode or null
node.getValue(MyType.class) // run the registered serializer
node.getValue(MyType.class, fallback)

isNull() is the cheapest way to probe whether an optional field exists. The convention is: node.node("optional").isNull() ? defaultValue : node.node("optional").getInt().

For list and map nodes you’ll use childrenList() / childrenMap() constantly - they’re how the bundled serializers walk through items, actions, and bindings.