MapStruct is a powerful Java annotation processor that simplifies the mapping between Java bean types. In this advanced tutorial, we will explore several advanced features of MapStruct, such as mapping from multiple sources to a target object, using Java expressions in mappings and much more!
Prerequisites
Before diving into the examples, ensure you have checked this introduction article to Mapstruct: How to Map Your DTO Objects with MapStruct
Then, make sure you have the following dependencies in your pom.xml if you are using Maven:
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.3.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.3.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>
For Gradle, add the following to your build.gradle:
dependencies {
implementation 'org.mapstruct:mapstruct:1.6.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3.Final'
}
1. Mapping from Multiple Sources
MapStruct allows you to map data from multiple source objects into a single target object. This is particularly useful when you need to aggregate data from different sources.
Example
Suppose we have two source classes, Student and Address, and we want to create a DeliveryAddress object.
public class Student {
private String name;
private Long id;
// Getters and Setters
}
public class Address {
private String city;
private String state;
private int houseNo;
// Getters and Setters
}
public class DeliveryAddress {
private String name;
private String city;
private String state;
private int houseNumber;
// Getters and Setters
}
And here is the Mapper Interface which maps multiple source objects to DeliveryAddress:
@Mapper
public interface DeliveryAddressMapper {
@Mapping(source = "student.name", target = "name")
@Mapping(source = "address.city", target = "city")
@Mapping(source = "address.state", target = "state")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddress getDeliveryAddress(Student student, Address address);
}

Finally, here is how you can use your DeliveryAddressMapper to build a DeliveryAddress:
Student student = new Student();
student.setName("John Doe");
Address address = new Address();
address.setCity("New York");
address.setState("NY");
address.setHouseNo(123);
DeliveryAddressMapper mapper = Mappers.getMapper(DeliveryAddressMapper.class);
DeliveryAddress deliveryAddress = mapper.getDeliveryAddress(student, address);
2. Using Java Expressions in Mappings
MapStruct allows you to use Java expressions to perform more complex mappings.
Example
Suppose we want to calculate the length of a student’s name while mapping.
Mapper Interface with Expression
@Mapper
public interface StudentMapper {
@Mapping(target = "nameLength", expression = "java(student.getName().length())")
StudentDto toStudentDto(Student student);
}
public class StudentDto {
private int nameLength;
// Getters and Setters
}
Usage
Student student = new Student();
student.setName("John Doe");
StudentMapper mapper = Mappers.getMapper(StudentMapper.class);
StudentDto studentDto = mapper.toStudentDto(student);
3. Lazy Mapping with @AfterMapping
The @AfterMapping annotation allows you to perform additional processing after the mapping is done, which can be useful for lazy loading or further calculations.
Example
Suppose we want to set a default value after mapping.
Mapper Interface with After Mapping
@Mapper
public interface UserMapper {
UserDto userToUserDto(User user);
@AfterMapping
default void setDefaultValues(@MappingTarget UserDto userDto) {
if (userDto.getRole() == null) {
userDto.setRole("USER"); // Set default role if not provided
}
}
}
Usage
User user = new User();
user.setName("Jane Doe");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
4. Additional Advanced Features
4.1 Nested Mappings
MapStruct can handle nested mappings easily by specifying the path for nested properties.
@Mapper
public interface OrderMapper {
@Mapping(target = "customerName", source = "customer.name")
OrderDto orderToOrderDto(Order order);
}
4.2 Custom Mapping Methods
You can define custom methods for complex mappings.
@Mapper
public interface ProductMapper {
@Mappings({
@Mapping(target = "price", source = "product.price"),
@Mapping(target = "discountedPrice", expression = "java(calculateDiscount(product))")
})
ProductDto productToProductDto(Product product);
default double calculateDiscount(Product product) {
return product.getPrice() * 0.9; // Apply a 10% discount
}
}
This custom mapping demonstrates an advanced use of MapStruct, including field mapping and custom logic for transformations. Here’s an explanation:
What This Code Does:
The ProductMapper interface maps a Product object to a ProductDto object, while performing:
- Standard field mapping: Maps the
pricefield directly fromProducttoProductDto. - Custom field mapping: Calculates the
discountedPriceinProductDtousing a custom method (calculateDiscount).
Conclusion
In this advanced MapStruct tutorial, we explored various features that enhance object mapping in Java applications:
- Mapping from multiple sources into a single target object.
- Using Java expressions for complex field mappings.
- Implementing lazy mapping with the
@AfterMappingannotation. - Additional features like nested mappings and custom methods.
By leveraging these advanced features, you can create efficient and maintainable mappings in your Java applications using MapStruct.