Using snake case with mapstruct

Using snake case with mapstruct

·

3 min read

Introduction

For those backend devs who are unfamiliar with mapstruct: you miss out on a fantastic tool. MapStruct is an open-source code generation library for mapping data between Java beans. It simplifies the process of converting one Java object (source object) into another Java object (target object). It offers immense possibilities to map one bean property into another. If you ever needed to write these mappings by hand, including type safety and null checks, then you know it's painstaking work.

But occasionally you need to interface with an API that provides data objects which not follow the Bean conventions. For example, an API that offers snake case objects.

Fortunately, mapstruct can be customized, such that it can recognize snake case (or any other convention) as well. This article will show you how.

Mapstruct SPI

The mapstruct SPI - Service Provider Interface - can customize the recognition of getter and setter methods. This can be done using a custom accessor naming strategy. This is explained in the mapstruct documentation. Below is the code snippet for a custom accessor naming strategy.

public class SnakeCamelCaseAccessorNamingStrategy extends DefaultAccessorNamingStrategy {

    @Override
    public boolean isGetterMethod(final ExecutableElement method) {
        final String methodName = method.getSimpleName().toString();
        return methodName.startsWith("get") && method.getReturnType().getKind() != TypeKind.VOID;
    }

    @Override
    public boolean isSetterMethod(final ExecutableElement method) {
        final String methodName = method.getSimpleName().toString();
        return methodName.startsWith( "set") && method.getReturnType().getKind() == TypeKind.VOID;
    }

    @Override
    public String getPropertyName(final ExecutableElement getterOrSetterMethod) {
        final String methodName = getterOrSetterMethod.getSimpleName().toString();
        if (methodName.contains("_")) {
            // snake case -> remove get and underscore
            final String property = methodName.substring(4);
            return property.toLowerCase();
        } else {
            // camel case -> remove get
            final String property = methodName.substring(3);
            return Character.toLowerCase(property.charAt(0)) + property.substring(1);
        }
    }
}

By overriding the isGetter, isSetter and getPropertyName we can implement custom logic that will recognize the getters, setters and properties - in this example for snake case.

Using the custom SPI

To use the customized AccessorNamingStrategy a jar must be created. This jar must include a file named after the custom naming strategy with the full package name, so in this example nl.devgs.mapstruct.spi.SnakeCamelCaseAccessorNamingStrategy. This file must be located in META-INF/services.

This jar then must be present on the classpath of the mapstruct annotation processor. The pom of a project that uses the custom accessor naming has the following compiler plugin configuration:

          <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <path>
                            <groupId>nl.devgs</groupId>
                            <artifactId>mapstruct-spi</artifactId>
                            <version>1.0</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${org.projectlombok.version}</version>
                        </path>
                        <dependency>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>0.2.0</version>
                        </dependency>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>

In the sample above, mapstruct is used next to Lombok.

Conclusion and pitfalls

Mapstruct offers great customization options, so it can be used not only for bean mappings. By doing a small customization, non-bean mappings can be done with great ease as well.

However, when implementing the custom naming accessor, it seems not possible to use a third-party jar like Apache commons. Using StringUtils from Apache offers many possibilities to do null-safe String manipulations. But when using the custom accessor, a ClassNotFound was thrown in the mapstruct processor, even when the library was included in the jar. So it looks like the Java string methods need to be used here.

The code for the custom accessor can be found here.

The code for using the accessor can be found here.