-
-
Notifications
You must be signed in to change notification settings - Fork 68
Object Mapper
The object mapper is a specialized TypeSerializer
that can extract information from Java objects to be able to serialize and deserialize them from ConfigurationNode
s.
The object mapper can only create objects that have no-arg constructor, are record
classes, or with the Kotlin extras module data
classes, but is otherwise restricted to populating already existing objects (with a ObjectMapper.Mutable
instance). It's generally easier to work with immutable data types, which is why the method assuming mutability is only exposed in a subtype.
Property names can be transformed between the config file and the object being mapped onto. The default behavior is to map kebab-case
in a config file to camelCase
in an object. This can be adjusted in the object mapper factory options, or it can be overridden on a particular property using the @Setting
annotation.
The object mapper supports any of the standard type serializers registered with a node's type serializer collection.
As of version 4.0, the object mapper has defined extension points, set up on an ObjectMapper.Factory.Builder
. See their class documentation for details on what each one does.
To use customized object mapper factories, they must be registered with a TypeSerializerCollection
. For example:
public ConfigurationLoader<?> createLoader(final Path source) {
final ObjectMapper.Factory customFactory = ObjectMapper.factoryBuilder()
.addResolver(NodeResolver.onlyWithSetting())
.build();
return YamlConfigurationLoader.builder()
.path(source)
.defaultOptions(opts -> opts.serializers(build -> build.registerAnnotatedObjects(customFactory)))
.build;
}
Here's a example of a standalone ObjectMapper setup. This uses a few value types, some nodes, and shows that type parameters are interpreted where specified.
public final class ObjectMapperExample {
private ObjectMapperExample() {}
public static void main(final String[] args) throws ConfigurateException {
final Path file = Paths.get(args[0]);
final HoconConfigurationLoader loader = HoconConfigurationLoader.builder()
.path(file) // or url(), or source/sink
.build();
final CommentedConfigurationNode node = loader.load(); // Load from file
final MyConfiguration config = node.get(MyConfiguration.class); // Populate object
// Do whatever actions with the configuration, then...
config.itemName("Steve");
node.set(MyConfiguration.class, config); // Update the backing node
loader.save(node); // Write to the original file
}
@ConfigSerializable
static class MyConfiguration {
// Fields must be non-final to be modified
private @Nullable String itemName;
@Comment("Here is a comment to describe the purpose of this field")
private Pattern filter = Pattern.compile("cars?"); // Set defaults by initializing the field
// As long as custom classes are annotated with @ConfigSerializable, they can be nested as ordinary fields.
private List<Section> sections = new ArrayList<>();
// This won't be written to the file because it's marked as `transient`
private transient @MonotonicNonNull String decoratedName;
public @Nullable String itemName() {
return this.itemName;
}
public void itemName(final String itemName) {
this.itemName = requireNonNull(itemName, "itemName");
}
public Pattern filter() {
return this.filter;
}
public List<Section> sections() {
return this.sections;
}
public String decoratedItemName() {
if (this.decoratedName == null) {
this.decoratedName = "[" + this.itemName + "]";
}
return this.decoratedName;
}
}
@ConfigSerializable
static class Section {
private String name;
private UUID id;
// the ObjectMapper resolves settings based on fields -- these methods are provided as a convenience
public String name() {
return this.name;
}
public UUID id() {
return this.id;
}
}
}