Skip to content
Alexander Städing edited this page Dec 14, 2023 · 15 revisions

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 ConfigurationNodes.

Basic Usage

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.

Naming Scheme

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.

Supported Types

The object mapper supports any of the standard type serializers registered with a node's type serializer collection.

Extending

Extending the object mapper

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()
        .addNodeResolver(NodeResolver.onlyWithSetting())
        .build();
        
    return YamlConfigurationLoader.builder()
        .path(source)
        .defaultOptions(opts -> opts.serializers(build -> build.registerAnnotatedObjects(customFactory)))
        .build();
}

Bringing it all together

Here's an 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;
        }

    }

}