Generate Object Mapping Using MapStruct
Do you write a lot of mapping code to map between different object models? Learn about MapStruct, which simplifies this task by generating mapping code.
Join the DZone community and get the full member experience.
Join For FreeDo you need to write a lot of mapping code in order to map between different object models? MapStruct simplifies this task by generating mapping code. In this blog, you will learn some basic features of MapStruct. Enjoy!
Introduction
In a multi-layered application, one often has to write boilerplate code in order to map different object models. This can be a tedious and an error-prone task. MapStruct simplifies this task by generating the mapping code for you. It generates code during compile time and aims to generate the code as if it was written by you.
This blog will only give you a basic overview of how MapStruct can aid you, but it will be sufficient to give you a good impression of which problem it can solve for you.
If you are using IntelliJ as an IDE, you can also install the MapStruct Support Plugin which will assist you in using MapStruct.
Sources used in this blog can be found on GitHub.
Prerequisites
Prerequisites for this blog are:
- Basic Java knowledge, Java 21 is used in this blog
- Basic Spring Boot knowledge
Basic Application
The application used in this blog is a basic Spring Boot project. By means of a Rest API, a customer can be created and retrieved. In order to keep the API specification and source code in line with each other, you will use the openapi-generator-maven-plugin
. First, you write the OpenAPI specification and the plugin will generate the source code for you based on the specification. The OpenAPI specification consists out of two endpoints, one for creating a customer (POST) and one for retrieving the customer (GET). The customer consists of its name and some address data.
Customer:
type: object
properties:
firstName:
type: string
description: First name of the customer
minLength: 1
maxLength: 20
lastName:
type: string
description: Last name of the customer
minLength: 1
maxLength: 20
street:
type: string
description: Street of the customer
minLength: 1
maxLength: 20
number:
type: string
description: House number of the customer
minLength: 1
maxLength: 5
postalCode:
type: string
description: Postal code of the customer
minLength: 1
maxLength: 5
city:
type: string
description: City of the customer
minLength: 1
maxLength: 20
The CustomerController
implements the generated Controller interface. The OpenAPI maven plugin makes use of its own model. In order to transfer the data to the CustomerService
, DTOs are created. These are Java records. The CustomerDto
is:
public record CustomerDto(Long id, String firstName, String lastName, AddressDto address) {
}
The AddressDto
is:
public record AddressDto(String street, String houseNumber, String zipcode, String city) {
}
The domain itself is used within the Service and is a basic Java POJO. The Customer
domain is:
public class Customer {
private Long customerId;
private String firstName;
private String lastName;
private Address address;
// Getters and setters left out for brevity
}
The Address
domain is:
public class Address {
private String street;
private int houseNumber;
private String zipcode;
private String city;
// Getters and setters left out for brevity
}
In order to connect everything together, you will need to write mapper code for:
- Mapping between the API model and the DTO
- Mapping between the DTO and the domain
Mapping Between DTO and Domain
Add Dependency
In order to make use of MapStruct, it suffices to add the MapStruct Maven dependency and to add some configuration to the Maven Compiler plugin.
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
...
<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>
Create Mapper
The CustomerDto
, AddressDto
and the Customer
, Address
domains do not differ very much from each other.
CustomerDto
has anid
whileCustomer
has acustomerId
.AddressDto
has ahouseNumber
of the type String whileAddress
has ahouseNumber
of the type integer.
In order to create a mapper for this using MapStruct, you create an interface CustomerMapper
, annotate it with @Mapper
, and specify the component model with the value spring. Doing this will ensure that the generated mapper is a singleton-scoped Spring bean that can be retrieved via @Autowired
.
Because both models are quite similar to each other, MapStruct will be able to generate most of the code by itself. Because the customer id has a different name in both models, you need to help MapStruct a bit. Using the @Mapping
annotation, you specify the source and target mapping. For the type conversion, you do not need to do anything, MapStruct can sort this out based on the implicit type conversions.
The corresponding mapper code is the following:
@Mapper(componentModel = "spring")
public interface CustomerMapper {
@Mapping(source = "customerId", target = "id")
CustomerDto transformToCustomerDto(Customer customer);
@Mapping(source = "id", target = "customerId")
Customer transformToCustomer(CustomerDto customerDto);
}
Generate the code:
$ mvn clean compile
In the target/generated-sources/annotations
directory, you can find the generated CustomerMapperImpl
class.
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2024-04-21T13:38:51+0200",
comments = "version: 1.5.5.Final, compiler: javac, environment: Java 21 (Eclipse Adoptium)"
)
@Component
public class CustomerMapperImpl implements CustomerMapper {
@Override
public CustomerDto transformToCustomerDto(Customer customer) {
if ( customer == null ) {
return null;
}
Long id = null;
String firstName = null;
String lastName = null;
AddressDto address = null;
id = customer.getCustomerId();
firstName = customer.getFirstName();
lastName = customer.getLastName();
address = addressToAddressDto( customer.getAddress() );
CustomerDto customerDto = new CustomerDto( id, firstName, lastName, address );
return customerDto;
}
@Override
public Customer transformToCustomer(CustomerDto customerDto) {
if ( customerDto == null ) {
return null;
}
Customer customer = new Customer();
customer.setCustomerId( customerDto.id() );
customer.setFirstName( customerDto.firstName() );
customer.setLastName( customerDto.lastName() );
customer.setAddress( addressDtoToAddress( customerDto.address() ) );
return customer;
}
protected AddressDto addressToAddressDto(Address address) {
if ( address == null ) {
return null;
}
String street = null;
String houseNumber = null;
String zipcode = null;
String city = null;
street = address.getStreet();
houseNumber = String.valueOf( address.getHouseNumber() );
zipcode = address.getZipcode();
city = address.getCity();
AddressDto addressDto = new AddressDto( street, houseNumber, zipcode, city );
return addressDto;
}
protected Address addressDtoToAddress(AddressDto addressDto) {
if ( addressDto == null ) {
return null;
}
Address address = new Address();
address.setStreet( addressDto.street() );
if ( addressDto.houseNumber() != null ) {
address.setHouseNumber( Integer.parseInt( addressDto.houseNumber() ) );
}
address.setZipcode( addressDto.zipcode() );
address.setCity( addressDto.city() );
return address;
}
}
As you can see, the code is very readable and it has taken into account the mapping of Customer
and Address
.
Create Service
The Service will create a domain Customer
taken the CustomerDto
as an input. The customerMapper
is injected into the Service and is used for converting between the two models. The other way around, when a customer is retrieved, the mapper converts the domain Customer
to a CustomerDto
. In the Service, the customers are persisted in a basic list in order to keep things simple.
@Service
public class CustomerService {
private final CustomerMapper customerMapper;
private final HashMap<Long, Customer> customers = new HashMap<>();
private Long index = 0L;
CustomerService(CustomerMapper customerMapper) {
this.customerMapper = customerMapper;
}
public CustomerDto createCustomer(CustomerDto customerDto) {
Customer customer = customerMapper.transformToCustomer(customerDto);
customer.setCustomerId(index);
customers.put(index, customer);
index++;
return customerMapper.transformToCustomerDto(customer);
}
public CustomerDto getCustomer(Long customerId) {
if (customers.containsKey(customerId)) {
return customerMapper.transformToCustomerDto(customers.get(customerId));
} else {
return null;
}
}
}
Test Mapper
The mapper can be easily tested by using the generated CustomerMapperImpl
class and verify whether the mappings are executed successfully.
class CustomerMapperTest {
@Test
void givenCustomer_whenMaps_thenCustomerDto() {
CustomerMapperImpl customerMapper = new CustomerMapperImpl();
Customer customer = new Customer();
customer.setCustomerId(2L);
customer.setFirstName("John");
customer.setLastName("Doe");
Address address = new Address();
address.setStreet("street");
address.setHouseNumber(42);
address.setZipcode("zipcode");
address.setCity("city");
customer.setAddress(address);
CustomerDto customerDto = customerMapper.transformToCustomerDto(customer);
assertThat( customerDto ).isNotNull();
assertThat(customerDto.id()).isEqualTo(customer.getCustomerId());
assertThat(customerDto.firstName()).isEqualTo(customer.getFirstName());
assertThat(customerDto.lastName()).isEqualTo(customer.getLastName());
AddressDto addressDto = customerDto.address();
assertThat(addressDto.street()).isEqualTo(address.getStreet());
assertThat(addressDto.houseNumber()).isEqualTo(String.valueOf(address.getHouseNumber()));
assertThat(addressDto.zipcode()).isEqualTo(address.getZipcode());
assertThat(addressDto.city()).isEqualTo(address.getCity());
}
@Test
void givenCustomerDto_whenMaps_thenCustomer() {
CustomerMapperImpl customerMapper = new CustomerMapperImpl();
AddressDto addressDto = new AddressDto("street", "42", "zipcode", "city");
CustomerDto customerDto = new CustomerDto(2L, "John", "Doe", addressDto);
Customer customer = customerMapper.transformToCustomer(customerDto);
assertThat( customer ).isNotNull();
assertThat(customer.getCustomerId()).isEqualTo(customerDto.id());
assertThat(customer.getFirstName()).isEqualTo(customerDto.firstName());
assertThat(customer.getLastName()).isEqualTo(customerDto.lastName());
Address address = customer.getAddress();
assertThat(address.getStreet()).isEqualTo(addressDto.street());
assertThat(address.getHouseNumber()).isEqualTo(Integer.valueOf(addressDto.houseNumber()));
assertThat(address.getZipcode()).isEqualTo(addressDto.zipcode());
assertThat(address.getCity()).isEqualTo(addressDto.city());
}
}
Mapping Between API and DTO
Create Mapper
The API model looks a bit different than the CustomerDto
because it has no Address
object and number
and postalCode
have different names in the CustomerDto
.
public class Customer {
private String firstName;
private String lastName;
private String street;
private String number;
private String postalCode;
private String city;
// Getters and setters left out for brevity
}
In order to create a mapper, you need to add a bit more @Mapping
annotations, just like you did before for the customer ID.
@Mapper(componentModel = "spring")
public interface CustomerPortMapper {
@Mapping(source = "street", target = "address.street")
@Mapping(source = "number", target = "address.houseNumber")
@Mapping(source = "postalCode", target = "address.zipcode")
@Mapping(source = "city", target = "address.city")
CustomerDto transformToCustomerDto(Customer customerApi);
@Mapping(source = "id", target = "customerId")
@Mapping(source = "address.street", target = "street")
@Mapping(source = "address.houseNumber", target = "number")
@Mapping(source = "address.zipcode", target = "postalCode")
@Mapping(source = "address.city", target = "city")
CustomerFullData transformToCustomerApi(CustomerDto customerDto);
}
Again, the generated CustomerPortMapperImpl
class can be found in the target/generated-sources/annotations
directory after invoking the Maven compile target.
Create Controller
The mapper is injected in the Controller and the corresponding mappers can easily be used.
@RestController
class CustomerController implements CustomerApi {
private final CustomerPortMapper customerPortMapper;
private final CustomerService customerService;
CustomerController(CustomerPortMapper customerPortMapper, CustomerService customerService) {
this.customerPortMapper = customerPortMapper;
this.customerService = customerService;
}
@Override
public ResponseEntity<CustomerFullData> createCustomer(Customer customerApi) {
CustomerDto customerDtoIn = customerPortMapper.transformToCustomerDto(customerApi);
CustomerDto customerDtoOut = customerService.createCustomer(customerDtoIn);
return ResponseEntity.ok(customerPortMapper.transformToCustomerApi(customerDtoOut));
}
@Override
public ResponseEntity<CustomerFullData> getCustomer(Long customerId) {
CustomerDto customerDtoOut = customerService.getCustomer(customerId);
return ResponseEntity.ok(customerPortMapper.transformToCustomerApi(customerDtoOut));
}
}
Test Mapper
A unit test is created in a similar way as the one for the Service and can be viewed here.
In order to test the complete application, an integration test is created for creating a customer.
@SpringBootTest
@AutoConfigureMockMvc
class CustomerControllerIT {
@Autowired
private MockMvc mockMvc;
@Test
void whenCreateCustomer_thenReturnOk() throws Exception {
String body = """
{
"firstName": "John",
"lastName": "Doe",
"street": "street",
"number": "42",
"postalCode": "1234",
"city": "city"
}
""";
mockMvc.perform(post("/customer")
.contentType("application/json")
.content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("firstName", equalTo("John")))
.andExpect(jsonPath("lastName", equalTo("Doe")))
.andExpect(jsonPath("customerId", equalTo(0)))
.andExpect(jsonPath("street", equalTo("street")))
.andExpect(jsonPath("number", equalTo("42")))
.andExpect(jsonPath("postalCode", equalTo("1234")))
.andExpect(jsonPath("city", equalTo("city")));
}
}
Conclusion
MapStruct is an easy-to-use library for mapping between models. If the basic mapping is not sufficient, you are even able to create your own custom mapping logic (which is not demonstrated in this blog). It is advised to read the official documentation to get a comprehensive list of all available features.
Published at DZone with permission of Gunter Rotsaert, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments