MapStruct: Initialization

Daniel S. Blanco
3 min readMar 19, 2022

--

Recently I have been migrating a very old project that made copies of values from a persistent object to a view layer object or DTO. The migration had several problems associated with that logic:

  • There were objects that did not have the same attributes.
  • With the migration and updating of the project, it could happen that the persistent classes, now JPA entities, would use more modern classes such as java.time.LocalDate and the DTO objects would continue to use more antiquated APIs and classes, such as java.util.Date.
  • They would use more basic and obsolete frameworks such as commons-beanutils.

Currently, there are several frameworks that can help us in this task such as Orika, ModelMapper, or JMapper. But we will focus on MapStruct. And for this, we will use version 1.4.2.Final.

Apart from the fact that in terms of performance it is one of the best libraries, another point in favor is that with the use of annotations the converter is easily configured. But this means that we have to add a minimum of configuration in the compilation plugin. In it, we must indicate the annotation processor to use, in this case, mapstruct’s own annotation processor.

But if we use lombok, we have to take into account that this library has its own annotation processor and therefore we will have to indicate both in the plugin:

We will see the different qualities through the conversion of two classes. On the one hand, the persistence classes:

And on the other hand, the DTO classes:

The operation is very simple. We will create an interface that has the annotation @Mapper and an attribute of the same type generated with the utility class of mapStruct, Mappers. And to use it, we will create methods that convert one type of object to another.

@Mapper
public interface BookConverter {
BookConverter MAPPER = Mappers.getMapper(BookConverter.class);
}

If we want a basic conversion, where the attributes of both classes, even if they do not match in type if they match in name, no extra configuration is needed. In the official documentation, the automatic conversions are also indicated, you can see them here. Also, no configuration is needed if a property does not exist or is null. The conversion will be performed without throwing any exception as it happens with other mappers.

SagaDto convertSaga(Saga sourceSaga);

In the case where there is a mapping that we want to be performed automatically but in which the names do not match, we will need an extra configuration. This can be done through the @Mapping annotation, in which we will indicate which attributes we want to be mapped automatically.

@Mapping(source = "saga", target = "serie")
BookDto convertBook(Book sourceBook);

We can also create an object based on two other objects. And map concrete attributes of concrete classes through the use of expressions and points. As we can see below.

@Mapping(source = "sourceBook.name", target = "bookName")
@Mapping(source = "sourceSaga.name", target = "sagaName")
BookResumeDto convertResume(Book sourceBook, Saga sourceSaga);

Through the @InheritInverseConfiguration annotation, we will be able to do the reverse mapping without having to configure anything if we have already done it in another method.

@InheritInverseConfiguration
Book convertBookDto(BookDto sourceBook);

To finish with the post, you can also generate your own methods of conversion within the interface and that is used by the instance that performs the mappings. Or even create classes that do the conversion in a specific way for certain objects and associate them in the main annotation:

@Mapper(uses=CustomDateMapper.class)

And apart from these, there are also other features that can help conversion. All of them are equally easy to use.

--

--