MapStruct: a Java Bean mapper
Credits : https://www.baeldung.com/mapstruct
1. Overview
In this tutorial, we’ll explore the use of MapStruct, which is, simply put, a Java Bean mapper.
This API contains functions that automatically map between two Java Beans. With MapStruct, we only need to create the interface, and the library will automatically create a concrete implementation during compile time.
Further reading:
Custom Mapper with MapStruct
Learn how to use custom mapper with the MapStruct library
Ignoring Unmapped Properties with MapStruct
MapStruct allows us to copy between Java beans. There are a few ways we can configure it to handle missing fields.
Using Multiple Source Objects with MapStruct
Learn how to use multiple source objects with MapStruct.
2. MapStruct and Transfer Object Pattern
For most applications, you’ll notice a lot of boilerplate code converting POJOs to other POJOs.
For example, a common type of conversion happens between persistence-backed entities and DTOs that go out to the client-side.
So, that is the problem that MapStruct solves: Manually creating bean mappers is time-consuming. But the library can generate bean mapper classes automatically.
3. Maven
Let’s add the below dependency into our Maven pom.xml:
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.4.2.Final</version> </dependency>
The latest stable release of MapStruct and its processor are both available from the Maven Central Repository.
Let’s also add the annotationProcessorPaths section to the configuration part of the maven-compiler-plugin plugin.
The mapstruct-processor is used to generate the mapper implementation during the build:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.4.2.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin>
4. Basic Mapping
4.1. Creating a POJO
Let’s first create a simple Java POJO:
public class SimpleSource { private String name; private String description; // getters and setters } public class SimpleDestination { private String name; private String description; // getters and setters }
4.2. The Mapper Interface
@Mapper public interface SimpleSourceDestinationMapper { SimpleDestination sourceToDestination(SimpleSource source); SimpleSource destinationToSource(SimpleDestination destination); }
Notice we did not create an implementation class for our SimpleSourceDestinationMapper — because MapStruct creates it for us.
4.3. The New Mapper
We can trigger the MapStruct processing by executing an mvn clean install.
This will generate the implementation class under /target/generated-sources/annotations/.
Here is the class that MapStruct auto-creates for us:
public class SimpleSourceDestinationMapperImpl implements SimpleSourceDestinationMapper { @Override public SimpleDestination sourceToDestination(SimpleSource source) { if ( source == null ) { return null; } SimpleDestination simpleDestination = new SimpleDestination(); simpleDestination.setName( source.getName() ); simpleDestination.setDescription( source.getDescription() ); return simpleDestination; } @Override public SimpleSource destinationToSource(SimpleDestination destination){ if ( destination == null ) { return null; } SimpleSource simpleSource = new SimpleSource(); simpleSource.setName( destination.getName() ); simpleSource.setDescription( destination.getDescription() ); return simpleSource; } }
4.4. Test Case
Finally, with everything generated, let’s write a test case showing that values in SimpleSource match values in SimpleDestination:
public class SimpleSourceDestinationMapperIntegrationTest { private SimpleSourceDestinationMapper mapper = Mappers.getMapper(SimpleSourceDestinationMapper.class); @Test public void givenSourceToDestination_whenMaps_thenCorrect() { SimpleSource simpleSource = new SimpleSource(); simpleSource.setName("SourceName"); simpleSource.setDescription("SourceDescription"); SimpleDestination destination = mapper.sourceToDestination(simpleSource); assertEquals(simpleSource.getName(), destination.getName()); assertEquals(simpleSource.getDescription(), destination.getDescription()); } @Test public void givenDestinationToSource_whenMaps_thenCorrect() { SimpleDestination destination = new SimpleDestination(); destination.setName("DestinationName"); destination.setDescription("DestinationDescription"); SimpleSource source = mapper.destinationToSource(destination); assertEquals(destination.getName(), source.getName()); assertEquals(destination.getDescription(), source.getDescription()); } }
5. Mapping With Dependency Injection
Next, let’s obtain an instance of a mapper in MapStruct by merely calling Mappers.getMapper(YourClass.class).
Of course, that’s a very manual way of getting the instance. However, a much better alternative is to inject the mapper directly where we need it (if our project uses any Dependency Injection solution).
Luckily, MapStruct has solid support for both Spring and CDI (Contexts and Dependency Injection).
To use Spring IoC in our mapper, we need to add the componentModel attribute to @Mapper with the value spring, and for CDI, it would be cdi.
5.1. Modify the Mapper
Add the following code to SimpleSourceDestinationMapper:
@Mapper(componentModel = "spring") public interface SimpleSourceDestinationMapper
5.2. Inject Spring Components into the Mapper
Sometimes, we’ll need to utilize other Spring components inside our mapping logic. In this case, we have to use an abstract class instead of an interface:
@Mapper(componentModel = "spring") public abstract class SimpleDestinationMapperUsingInjectedService
Then, we can easily inject the desired component using a well-known @Autowired annotation and use it in our code:
@Mapper(componentModel = "spring") public abstract class SimpleDestinationMapperUsingInjectedService { @Autowired protected SimpleService simpleService; @Mapping(target = "name", expression = "java(simpleService.enrichName(source.getName()))") public abstract SimpleDestination sourceToDestination(SimpleSource source); }
We must remember not to make the injected bean private! This is because MapStruct has to access the object in the generated implementation class.
6. Mapping Fields With Different Field Names
From our previous example, MapStruct was able to map our beans automatically because they have the same field names. So, what if a bean we are about to map has a different field name?
In this example, we will be creating a new bean called Employee and EmployeeDTO.
6.1. New POJOs
public class EmployeeDTO { private int employeeId; private String employeeName; // getters and setters }
public class Employee { private int id; private String name; // getters and setters }
6.2. The Mapper Interface
When mapping different field names, we will need to configure its source field to its target field and to do that, we will need to add @Mapping annotation for each field.
In MapStruct, we can also use dot notation to define a member of a bean:
@Mapper public interface EmployeeMapper { @Mapping(target="employeeId", source="entity.id") @Mapping(target="employeeName", source="entity.name") EmployeeDTO employeeToEmployeeDTO(Employee entity); @Mapping(target="id", source="dto.employeeId") @Mapping(target="name", source="dto.employeeName") Employee employeeDTOtoEmployee(EmployeeDTO dto); }
6.3. Test Case
Again, we need to test that both source and destination object values match:
@Test public void givenEmployeeDTOwithDiffNametoEmployee_whenMaps_thenCorrect() { EmployeeDTO dto = new EmployeeDTO(); dto.setEmployeeId(1); dto.setEmployeeName("John"); Employee entity = mapper.employeeDTOtoEmployee(dto); assertEquals(dto.getEmployeeId(), entity.getId()); assertEquals(dto.getEmployeeName(), entity.getName()); }
More test cases can be found in the GitHub project.
7. Mapping Beans With Child Beans
Next, we’ll show how to map a bean with references to other beans.
7.1. Modify the POJO
Let’s add a new bean reference to the Employee object:
public class EmployeeDTO { private int employeeId; private String employeeName; private DivisionDTO division; // getters and setters omitted }
public class Employee { private int id; private String name; private Division division; // getters and setters omitted }
public class Division { private int id; private String name; // default constructor, getters and setters omitted }
7.2. Modify the Mapper
Here we need to add a method to convert the Division to DivisionDTO and vice versa; if MapStruct detects that the object type needs to be converted and the method to convert exists in the same class, it will use it automatically.
Let’s add this to the mapper:
DivisionDTO divisionToDivisionDTO(Division entity); Division divisionDTOtoDivision(DivisionDTO dto);
7.3. Modify the Test Case
Let’s modify and add a few test cases to the existing one:
@Test public void givenEmpDTONestedMappingToEmp_whenMaps_thenCorrect() { EmployeeDTO dto = new EmployeeDTO(); dto.setDivision(new DivisionDTO(1, "Division1")); Employee entity = mapper.employeeDTOtoEmployee(dto); assertEquals(dto.getDivision().getId(), entity.getDivision().getId()); assertEquals(dto.getDivision().getName(), entity.getDivision().getName()); }
8. Mapping With Type Conversion
MapStruct also offers a couple of ready-made implicit type conversions, and for our example, we will try to convert a String date to an actual Date object.
For more details on implicit type conversion, check out the MapStruct reference guide.
8.1. Modify the Beans
We add a start date for our employee:
public class Employee { // other fields private Date startDt; // getters and setters }
public class EmployeeDTO { // other fields private String employeeStartDt; // getters and setters }
8.2. Modify the Mapper
We modify the mapper and provide the dateFormat for our start date:
@Mapping(target="employeeId", source = "entity.id") @Mapping(target="employeeName", source = "entity.name") @Mapping(target="employeeStartDt", source = "entity.startDt", dateFormat = "dd-MM-yyyy HH:mm:ss") EmployeeDTO employeeToEmployeeDTO(Employee entity); @Mapping(target="id", source="dto.employeeId") @Mapping(target="name", source="dto.employeeName") @Mapping(target="startDt", source="dto.employeeStartDt", dateFormat="dd-MM-yyyy HH:mm:ss") Employee employeeDTOtoEmployee(EmployeeDTO dto);
8.3. Modify the Test Case
Let’s add a few more test cases to verify the conversion is correct:
private static final String DATE_FORMAT = "dd-MM-yyyy HH:mm:ss"; @Test public void givenEmpStartDtMappingToEmpDTO_whenMaps_thenCorrect() throws ParseException { Employee entity = new Employee(); entity.setStartDt(new Date()); EmployeeDTO dto = mapper.employeeToEmployeeDTO(entity); SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT); assertEquals(format.parse(dto.getEmployeeStartDt()).toString(), entity.getStartDt().toString()); } @Test public void givenEmpDTOStartDtMappingToEmp_whenMaps_thenCorrect() throws ParseException { EmployeeDTO dto = new EmployeeDTO(); dto.setEmployeeStartDt("01-04-2016 01:00:00"); Employee entity = mapper.employeeDTOtoEmployee(dto); SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT); assertEquals(format.parse(dto.getEmployeeStartDt()).toString(), entity.getStartDt().toString()); }
9. Mapping With an Abstract Class
Sometimes, we may want to customize our mapper in a way that exceeds @Mapping capabilities.
For example, in addition to type conversion, we may want to transform the values in some way, as in our example below.
In such cases, we can create an abstract class and implement methods we want to have customized, and leave abstract those that should be generated by MapStruct.
9.1. Basic Model
In this example, we’ll use the following class:
public class Transaction { private Long id; private String uuid = UUID.randomUUID().toString(); private BigDecimal total; //standard getters }
and a matching DTO:
public class TransactionDTO { private String uuid; private Long totalInCents; // standard getters and setters }
The tricky part here is converting the BigDecimal total amount of dollars into a Long totalInCents.
9.2. Defining a Mapper
We can achieve this by creating our Mapper as an abstract class:
@Mapper abstract class TransactionMapper { public TransactionDTO toTransactionDTO(Transaction transaction) { TransactionDTO transactionDTO = new TransactionDTO(); transactionDTO.setUuid(transaction.getUuid()); transactionDTO.setTotalInCents(transaction.getTotal() .multiply(new BigDecimal("100")).longValue()); return transactionDTO; } public abstract List<TransactionDTO> toTransactionDTO( Collection<Transaction> transactions); }
Here, we’ve implemented our fully customized mapping method for a single object conversion.
On the other hand, we left the method, which is meant to map Collection to a List abstract, so MapStruct will implement it for us.
9.3. Generated Result
Since we have already implemented the method to map a single Transaction into a TransactionDTO, we expect MapStruct to use it in the second method.
The following will be generated:
@Generated class TransactionMapperImpl extends TransactionMapper { @Override public List<TransactionDTO> toTransactionDTO(Collection<Transaction> transactions) { if ( transactions == null ) { return null; } List<TransactionDTO> list = new ArrayList<>(); for ( Transaction transaction : transactions ) { list.add( toTransactionDTO( transaction ) ); } return list; } }
As we can see in line 12, MapStruct uses our implementation in the method that is generated.
10. Before-Mapping and After-Mapping Annotations
Here’s another way to customize @Mapping capabilities by using @BeforeMapping and @AfterMapping annotations. The annotations are used to mark methods that are invoked right before and after the mapping logic.
They are quite useful in scenarios where we might want this behavior to be applied to all mapped super-types.
Let’s take a look at an example that maps the sub-types of Car ElectricCar and BioDieselCar to CarDTO.
While mapping, we would like to map the notion of types to the FuelType enum field in the DTO. Then after the mapping is done, we’d like to change the name of the DTO to uppercase.
10.1. Basic Model
We’ll use the following classes:
public class Car { private int id; private String name; }
Sub-types of Car:
public class BioDieselCar extends Car { }
public class ElectricCar extends Car { }
The CarDTO with an enum field type FuelType:
public class CarDTO { private int id; private String name; private FuelType fuelType; }
public enum FuelType { ELECTRIC, BIO_DIESEL }
10.2. Defining the Mapper
Now let’s proceed and write our abstract mapper class that maps Car to CarDTO:
@Mapper public abstract class CarsMapper { @BeforeMapping protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) { if (car instanceof ElectricCar) { carDto.setFuelType(FuelType.ELECTRIC); } if (car instanceof BioDieselCar) { carDto.setFuelType(FuelType.BIO_DIESEL); } } @AfterMapping protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) { carDto.setName(carDto.getName().toUpperCase()); } public abstract CarDTO toCarDto(Car car); }
@MappingTarget is a parameter annotation that populates the target mapping DTO right before the mapping logic is executed in case of @BeforeMapping and right after in case of @AfterMapping annotated method.
10.3. Result
The CarsMapper defined above generates the implementation:
@Generated public class CarsMapperImpl extends CarsMapper { @Override public CarDTO toCarDto(Car car) { if (car == null) { return null; } CarDTO carDTO = new CarDTO(); enrichDTOWithFuelType(car, carDTO); carDTO.setId(car.getId()); carDTO.setName(car.getName()); convertNameToUpperCase(carDTO); return carDTO; } }
Notice how the annotated methods invocations surround the mapping logic in the implementation.
11. Support for Lombok
In the recent version of MapStruct, Lombok support was announced. So, we can easily map a source entity and a destination using Lombok.
To enable Lombok support, we need to add the dependency in the annotation processor path. Since Lombok version 1.18.16, we also have to add the dependency on lombok-mapstruct-binding. Now we have the mapstruct-processor as well as Lombok in the Maven compiler plugin:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.4.2.Final</version> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok-mapstruct-binding</artifactId> <version>0.2.0</version> </path> </annotationProcessorPaths> </configuration> </plugin>
Let’s define the source entity using Lombok annotations:
@Getter @Setter public class Car { private int id; private String name; }
And the destination data transfer object:
@Getter @Setter public class CarDTO { private int id; private String name; }
The mapper interface for this remains similar to our previous example:
@Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); CarDTO carToCarDTO(Car car); }
12. Support for defaultExpression
Starting with version 1.3.0, we can use the defaultExpression attribute of the @Mapping annotation to specify an expression that determines the value of the destination field if the source field is null. This is in addition to the existing defaultValue attribute functionality.
The source entity:
public class Person { private int id; private String name; }
The destination data transfer object:
public class PersonDTO { private int id; private String name; }
If the id field of the source entity is null, we want to generate a random id and assign it to the destination keeping other property values as-is:
@Mapper public interface PersonMapper { PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class); @Mapping(target = "id", source = "person.id", defaultExpression = "java(java.util.UUID.randomUUID().toString())") PersonDTO personToPersonDTO(Person person); }
Let’s add a test case to verify the expression execution:
@Test public void givenPersonEntitytoPersonWithExpression_whenMaps_thenCorrect() Person entity = new Person(); entity.setName("Micheal"); PersonDTO personDto = PersonMapper.INSTANCE.personToPersonDTO(entity); assertNull(entity.getId()); assertNotNull(personDto.getId()); assertEquals(personDto.getName(), entity.getName()); }
13. Conclusion
This article provided an introduction to MapStruct. We’ve introduced most of the basics of the Mapping library and how to use it in our applications.
The implementation of these examples and tests can be found in the GitHub project. This is a Maven project, so it should be easy to import and run as it is.