Motivation
Most applications in the world follow a simple format. They ingest some data, do some transformation on it, and then expose this transformed data to their consumers. If we look at a high enough level, we will see this pattern in all applications. The transformation part can be very complex and follow a lot of business logic, or it could be very simple and consist of just renaming some fields, removing some fields or adding some fields. As we all can guess, writing code for simple transformation can feel very repetitive, boring and thus error-prone. So, it comes as no surprise that there are libraries out there, built to tackle this in an easier and simpler way. MapStruct is just one library like that one.
My team used it a long time ago, on a project where our application needed to read data from DB and expose it to the world over REST API. The problem was that we were not the owners of the DB and we were not able to change DB schema definitions and make data look in DB how it looked to end consumers of our API. So, we were faced with two possible solutions to our problem. Write a lot of boilerplate code to do transformations, and invoke a lot of maintenance costs on ourselves, or use a library that would do most of the heavy lifting for us. Our choice landed on MapStruct, and we were saved on multiple occasions by it, especially as the requirements of our consumers underwent massive changes. Changing transformations was easy because we only needed to change some settings and MapStruct did all the work for us automatically.
Let us take a look at MapStruct.
Context
In our example, we shall assume that we need to build a REST API that exposes some data that don’t belong to us. We are either receiving it via some 3rd party API or reading from some other team DB.
To make it self-contained, In our use case, we will assume that data is coming from DB. For simplicity reasons in this example, we will use H2, in the memory database to simulate this use case. Let us assume that we are interested in Customer data, which is stored in table Customers.
The first thing that we need to do, is to create a simple Java class Customer, mark it as Entity and map all columns from the table into this class. Once we are done, we will get some code like this.
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
private String firstName;
@NotNull
private String lastName;
@NotNull
private Integer dayOfBirth;
@NotNull
private Integer monthOfBirth;
@NotNull
private Integer yearOfBirth;
private String address;
private Integer houseNumber;
private String houseNumberAddition;
private String city;
private String country;
// getters and setters
.....
}
Unfortunately, our consumers are expecting data in different format. So, let us create class Customer2DTO which will be in the format that our consumers expect to receive the data.
We would end up with code like this.
public class Customer2DTO {
private Long id;
private String name;
private String familyName;
private String fullName;
private LocalDate birthDay;
private String address;
private Integer houseNumber;
private String houseNumberAddition;
private String city;
private String country;
// getters and setters
....
}
As we can see, some fields are the same in both Customer and Customer2DTO. However, some fields have different names in Customer2DTO like name and familyName. Also, there are fields in Customer2DTO that don’t exist in Customer, fullName and birthDay. Of course, we can write code that would copy all the same fields, and adjust for name changes, this would mean that we would increase the size of our code, and the amount of code that we now need to maintain and look after. A more elegant solution would be to use MapStruct, so let us see how we can do exactly that in this use case.
Adding MapStruct to our project
The first thing that we need to do, is to add MapStruct to our project as a dependency.
Let us add this piece of code in the dependencies block of our pom.xml file
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Now that MapStruct has been added to our project, and the build phase adjusted, we can start leveraging the power of MapStruct.
First usage of MapStruct
We need to create one interface, let us call it Customer2Mapper, and let us annotate it with @Mapper.
In this way, we are telling MapStruct to create an implemenation of this interface.
import org.mapstruct.Mapper;
@Mapper
public interface Customer2Mapper {
}
The next thing is to get INSTANCE of this interface that we can use in our code. For that, we need to add one line to our code.
Customer2Mapper INSTANCE = Mappers.getMapper(Customer2Mapper.class);
The final thing that we need to do is to create a method signature that will do the transformation from Customer to Customer2DTO.
Customer2DTO customerToCustomerDTO(Customer customer);
full code will look something like this
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer2DTO;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;
@Mapper
public interface Customer2Mapper {
Customer2Mapper INSTANCE = Mappers.getMapper(Customer2Mapper.class);
Customer2DTO customerToCustomerDTO(Customer customer);
}
To utilise MapStruct transformation, we just need to call this code on the instance of a Customer.
Customer2DTO c2dto = Customer2Mapper.INSTANCE.customerToCustomerDTO(customer);
In case we run this code and check what the result is, we will see that MapStruct did its best to map all the fields from Customer to Customer2DTO which have the same name and compatible types. In our case, all fields should be mapped, except name, familyName, fullName and birthDay.
Renaming fields
Let us see how we can use MapStruct to rename the fields.
All that we need to do, to tell MapStruct to map field firstName from Customer to field name in Customer2DTO, is to add this line before the method signature of customerToCustomerDTO
@Mapping(source = "firstName", target = "name")
here we are telling MapStruct to use field firstName in Customer as source and field name in Customer2DTO as target. MapStruct will generate code that will do exactly that. We can repeat this for as many fields as we want, we just need to match the correct source and target fields. So for mapping lastName to familyName we need to add this
@Mapping(source = "lastName", target = "familyName")
Combining multiple fields
In the case of the fullName, we need to combine two fields from Customer into one field in Customer2DTO. Again we will use annotation @Mapping. Parameter target will be fullName. However, we will not use source. Instead, we will use expresion. The code will look like this
@Mapping(target="fullName", expression = "java(customer.getFirstName() +\" \"+ customer.getLastName())")
We can put almost any Java code in expresion. However, it would make most sense to keep it simple and don’t go nuts. Think about the future you who will maintain this code :-).
To map the field birthDay in Customer2DTO, we will use a similar approach as for fullName. We will add this code
@Mapping(target="birthDay", expression = "java(java.time.LocalDate.of(customer.getYearOfBirth(), customer.getMonthOfBirth(), customer.getDayOfBirth()))")
so full code will look something like this
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer2DTO;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;
@Mapper
public interface Customer2Mapper {
Customer2Mapper INSTANCE = Mappers.getMapper(Customer2Mapper.class);
@Mapping(source = "firstName", target = "name")
@Mapping(source = "lastName", target = "familyName")
@Mapping(target="fullName", expression = "java(customer.getFirstName() +\" \"+ customer.getLastName())")
@Mapping(target="birthDay", expression = "java(java.time.LocalDate.of(customer.getYearOfBirth(), customer.getMonthOfBirth(), customer.getDayOfBirth()))")
Customer2DTO customerToCustomerDTO(Customer customer);
}
Creating “sub-objects” inside Data Transfer objects
Very often once we have done some work, requirements change or get extended. So let us assume that happened to our use case. Instead of getting data in the format of Customer2DTO, all of a sudden, we need to send data in a different format. First, we will create a class in a new format and will call it Customer3DTO. It would look something like this
public class HomeAddressDTO {
private String street;
private Integer houseNumber;
private String addition;
private String city;
private String country;
//getters and setters
....
}
public class Customer3DTO {
private Long id;
private String name;
private String familyName;
private String fullName;
private LocalDate birthDay;
private HomeAddressDTO homeAddress;
// getters and setters
....
}
as we can see, info about home address now needs to be an object in itself, instead of individual fields in response of our API.
Let us make a copy of our Mapper interface Customer2Mapper, and let us call it Customer3Mapper. The only thing that we need to do to create an instance of object HomeAddressDTO in Customer3DTO and fill in with appropriate data is to add a few mappings that we used in past with source and target arguments. Only this time values for the target will have the prefix “homeAddress.” . The result should look like this
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer3DTO;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;
@Mapper
public interface Customer3Mapper {
Customer3Mapper INSTANCE = Mappers.getMapper(Customer3Mapper.class);
@Mapping(source = "firstName", target = "name")
@Mapping(source = "lastName", target = "familyName")
@Mapping(target="fullName", expression = "java(customer.getFirstName() +\" \"+ customer.getLastName())")
@Mapping(target="birthDay", expression = "java(java.time.LocalDate.of(customer.getYearOfBirth(), customer.getMonthOfBirth(), customer.getDayOfBirth()))")
@Mapping(target="homeAddress.street",source="address")
@Mapping(target="homeAddress.houseNumber",source="houseNumber")
@Mapping(target="homeAddress.addition",source="houseNumberAddition")
@Mapping(target="homeAddress.city",source="city")
@Mapping(target="homeAddress.country",source = "country")
Customer3DTO customerToCustomerDTO(Customer customer);
}
If we run this code and check output we should see that all is as expected. We can easily check this by using simple JUnit tests, for example
public class DummyCustomerBuilder {
public static Customer dummyCustomer() {
Customer customer = new Customer();
customer.setId((long)1);
customer.setFirstName("Sherlock");
customer.setLastName("Holmes");
customer.setCity("London");
customer.setCountry("Great Britan");
customer.setHouseNumber(221);
customer.setHouseNumberAddition("B");
customer.setAddress("Baker Street");
customer.setDayOfBirth(6);
customer.setMonthOfBirth(1);
customer.setYearOfBirth(1854);
return customer;
}
}
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer3DTO;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;
import java.time.LocalDate;
public class Customer3MapperTest {
// .........
@Test
public void testMappingHomeAddress() {
//given
Customer customer = DummyCustomerBuilder.dummyCustomer();
//when
Customer3DTO cDto = Customer3Mapper.INSTANCE.customerToCustomerDTO(customer);
//then
Assertions.assertNotNull(cDto);
Assertions.assertNotNull(cDto.getHomeAddress());
Assertions.assertEquals("Baker Street",cDto.getHomeAddress().getStreet());
Assertions.assertEquals(Integer.valueOf(221), cDto.getHomeAddress().getHouseNumber());
Assertions.assertEquals("B",cDto.getHomeAddress().getAddition());
Assertions.assertEquals("London", cDto.getHomeAddress().getCity());
Assertions.assertEquals("Great Britan",cDto.getHomeAddress().getCountry());
}
}
Conclusion
As we saw from our simple realistic example, with very little code using MapStruct we can handle a lot of everyday transformation without the need to create a lot of boilerplate code. This means that maintaining and modifying code to meet the demands of tomorrow will be easier, due to the simple fact that there is less of it.
In this blog post, we just scratched the surface of all the things MapStruct can help us with, and I highly recommend checking [official website](https://mapstruct.org/) for more info.
My suggestion in day-to-day usage of MapStruct would be make sure to keep it simple and always think if something is easier and better to achieve using MapStruct or your custom code. The fact that you can do something using one, doesn’t always mean that you should. Maybe there is a simpler and better solution.
## Resources