-
-
Notifications
You must be signed in to change notification settings - Fork 68
Type Serializers
Type serializers are Configurate's mechanism for converting between types native to each configuration format and what is requested by a user. Configurate comes with a library of serializers that cover many common classes in the JDK. These are:
java.net.URI
java.net.URL
-
java.util.UUID
-- supporting UUIDs in both RFC and Mojang (without dashes) formats - Any class annotated with
@ConfigSerializable
-- processed through the Object Mapper -
byte
,short
,int
,long
,float
, anddouble
numbers, char
boolean
java.lang.String
-
java.util.Map
-- deserialized values are LinkedHashMap instances -
java.util.List
-- deserialized values are currently instances ofArrayList
- Any
enum
class java.util.regex.Pattern
- Any type of array -- primitive and
Object[]
-
java.util.Set
-- deserialized values are currently instances ofHashSet
-
ConfigurationNode
-- the underlying node will be copied. -
java.util.File
andjava.nio.file.Path
To be able to locate type serializers, they have to be registered for specific types. Standard serializers are included in the default collection, accessible at TypeSerializerCollection.defaults()
. Custom serializers have to be registered while building a child. Configurate provides two types of registration -- register
, which registers for the specified type and all its subtypes, and registerExact
, which only registers for the type itself.
While Configurate's stock serializers and object mappers should be the first choices for converting objects, sometimes it is necessary to perform custom serialization -- if you don't control the types being serialized, or Configurate needs to be decoupled from the types that are being serialized. In that case, custom implementations of TypeSerializer
can be created and registered.
Configurate provides AbstractListChildSerializer
and ScalarSerializer
as base classes for data types that are derived from a list data structure in a configuration and a scalar value respectively. They provide additional operations and validation that might be relevant for their respective value types.
As an example of a serializer, let's look at how an imaginary type, Beverage
, with the following API, would be serialized:
/* A drink. */
public interface Beverage {
/* Create a new drink description */
static Beverage of(final int temperature, final boolean sweet) {
// [...]
}
/* The beverage's temperature in degrees Celsius, between 0 and 100. */
int temperature();
/* Whether the beverage is sweet. */
boolean sweet();
}
Implementing a serializer is fairly straightforward, using the methods you're already familiar with from a ConfigurationNode
. This one does some basic validation on provided input as well:
final class BeverageSerializer implements TypeSerializer<Beverage> {
static final BeverageSerializer INSTANCE = new BeverageSerializer();
private static final String TEMPERATURE = "temperature";
private static final String SWEET = "sweet";
private BeverageSerializer() {
}
private ConfigurationNode nonVirtualNode(final ConfigurationNode source, final Object... path) throws SerializationException {
if (!source.hasChild(path)) {
throw new SerializationException("Required field " + Arrays.toString(path) + " was not present in node");
}
return source.node(path);
}
@Override
public Beverage deserialize(final Type type, final ConfigurationNode source) throws SerializationException {
final ConfigurationNode temperatureNode = nonVirtualNode(source, TEMPERATURE);
final int temperature = temperatureNode.getInt();
if (temperature < 0 || temperature > 100) { // beverages are liquid, which is between 0 and 100 degrees celsius
// Throw an exception that specifically refers to the temperature field and its type, not the containing node
throw new SerializationException(temperatureNode, int.class, "A beverage must be in liquid form, but this one had a temperature of " + temperature + "deg C!");
}
final boolean sweet = nonVirtualNode(source, SWEET).getBoolean();
return Beverage.of(temperature, sweet);
}
@Override
public void serialize(final Type type, final @Nullable Beverage bev, final ConfigurationNode target) throws SerializationException {
if (bev == null) {
target.raw(null);
return;
}
target.node(TEMPERATURE).set(bev.temperature());
target.node(SWEET).set(bev.sweet());
}
}
To use this serializer, simply register it when creating a new loader:
YamlConfigurationLoader.builder()
.defaultOptions(opts -> opts.serializers(build -> build.register(Beverage.class, BeverageSerializer.INSTANCE)))
// [... any other configuration ...]
Of course, this registration can be combined with any other customizations to the default options, or any further custom serializers.
The process is not much different when making serializers for distribution. However, the individual serializer classes should usually not be exposed API -- instead, a collection should be built containing custom serializers. This collection is then the only thing exposed in-api. Users can add all serializers at once using TypeSerializerCollection.Builder.registerAll()
.
For example, given an imaginary library called Axlotl
providing some serializers, end users could create a loader that used those serializers like so:
public ConfigurationLoader<?> createLoader() {
return HoconConfigurationLoader.builder()
.path(Paths.get("somefile.conf"))
.defaultOptions(opts -> opts.serializers(build -> build.registerAll(AxlotlSerializers.collection())))
.build()
}