Mapping immutable POJOs with MapStruct

Marcos Abel
Trabe
Published in
5 min readJul 29, 2019
Photo by Rahul Bhogal on Unsplash

In any non-trivial Java application, you are almost guaranteed to have the need of converting between different kinds of objects. A classic example of this need is the conversion from domain objects to API-specific representations…but this is just one example. Complex systems are comprised of multiple layers with different levels of abstraction and, at some point, we usually need to convert back and forth between those levels.

A while ago I wrote a story about code generation with Lombok project. Lombok allows us to write less code in our POJOs. We embrace immutability for our POJOs, so we usually generate builders and getters for them (@Builder and @Value).

For some time, we struggled to find a mapping library that supported mapping immutable objects and played nicely with our Lombok-generated POJOs. Luckily for us, MapStruct became the tool we were waiting for when the library added support for builders in v1.3.0.Beta1.

Why a mapping library

We can write the mapping logic ourselves. It’s not especially complex…but it can become a nightmare to maintain. A minor change in a POJO can lead to broken mappers.

Of course, we should have tests for our mappers but even assuming that these brittle tests cover all mapping scenarios and break when they are supposed to break, trivial changes in POJOs will always require modifications to our mappers.

Using a mapping library provides us with default behaviors for mappers and, as a result, some changes to our POJOs won’t break our mappers. Additionally, a mapping library allows us to write less code…and the code we don’t write is always the easiest to maintain.

Why MapStruct

We highlighted one of the reasons a couple of paragraphs ago: MapStruct plays nicely with Lombok and has the ability to map immutable objects using builders. But this is not the only reason for using MapStruct.

MapStruct is a code generation library, and as such, performs beautifully. Performance can or cannot be an issue depending on your requirements and the number of mapping operations your application performs…but a code generation approach seems like the best option if you care about performance.

Additionally, MapStruct integrates seamlessly with Spring which is our framework of choice for most Java applications.

Adding MapStruct to our project

We will need to add a dependency to our pom.xml:

and add the configuration for both MapStruct and Lombok annotation processors:

With no further ado, we can start mapping objects.

Basic example

Let’s say we have these two classes:

Our business logic states that when mapping a Product to an ApiProduct:

  • Product.externalId must be mapped to ApiProduct.id.
  • Product.name must be mapped to ApiProduct.caption.
  • Product.description must be mapped to ApiProduct.description.

MapStruct mappers are interfaces (annotated with @Mapper) where we define the mapping methods we want and include annotations to control the mapping process. The Mapstruct annotation processor will generate an implementation for our interface, following the instructions given by our annotations.

Let’s define a MapStruct mapper for our basic example:

As you can see we only need to include annotations for the peculiarities of our mapping scenario. MapStruct provides sensible defaults:

  • When a property has the same name in both source and target objects, it is mapped implicitly.
  • When a property exists in the source but not in the target, it’s ignored.

The annotation processor provided by Mapstruct will generate at compile time an implementation for our ProductMapper interface. The generated code looks like this:

As you can see, when we include the componentModel = “spring” property in the @Mapperannotation, the generated interface is a Spring @Component. This built-in support for Spring integration makes MapStruct mappers easy to use in Spring applications.

More than one source

Sometimes we need more than a source object for our conversion. MapStruct supports these scenarios.

Let’s say that ApiProduct includes now a categoryId field:

In our model, we have a Category POJO:

With these building blocks, we can add a new parameter to our mapper:

As you can see, we need now to fully qualify the source properties including the parameter name but, apart from that, the mapping process remains largely the same. The code generated by Mapstruct for this example will be:

Complex mappings

In some cases, we need more control over the mapping process. For these scenarios, MapStruct provides us with the ability to write custom Java expressions. Let’s say that our logic changes and ApiProduct.id should be now the String resulting of concatenating Product.id and Product.externalId. We can change ApiProduct.id type to String and then define that mapping as follows:

These expressions inside the @Mapping annotation are not exactly easy to read but we can refactor the code a bit and take advantage of default methods to make it more readable:

Of course, you can organize the code any way you want. You could extract these expressions to a custom component and inject that component in the mapper implementation. As you can see in the linked page, the mechanism provided by MapStruct doesn’t exactly work seamlessly in current versions for our use case (using the injected component in expressions) and you will be forced to convert your interface into an abstract class and autowire the custom component yourself.

If your mapper functions are pure functions, you can just move them to an interface and instruct MapStruct to import that interface for you making use of the imports attribute of the @Mapper annotation. This is usually our preferred approach. For our example, we can create the following interface:

And rewrite our mapper as follows:

Organization and readability apart, MapStruct expressions allow you to write custom code with full access to the mapping method parameters, so you can write any custom code you need.

Wrapping up

MapStruct is a code generation library that generates object mappers for you. It is easy to customize, performs well and plays nicely with immutable objects generated using Lombok project. In addition, MapStruct integrates seamlessly with Spring and, if you are an IntelliJ IDEA user, you even have a plugin to make life easier for you.

You can’t ask for much more. You have no excuse to keep writing (and maintaining) your own object mappers.

--

--