Back to blog
Feb 05, 2025
9 min read

Mapstruct : transforming complex objects

Mapstruct is a code generator which greatly simplifies the process of converting between different Java classes using a builder pattern.

Introduction

Mapstruct is a code generator for the Java programming language that greatly simplifies the process of converting between different Java classes using a builder pattern. When working with complex applications, especially those following clean architecture principles, we often need to transform objects between different layers (e.g., from entities to DTOs and vice versa). MapStruct makes this process efficient and maintainable.

The generated mapping code uses plain method invocations and thus is fast, type-safe and easy to understand.

Setup

Add the following dependencies to your pom.xml:

pom.xml - MapStruct Dependencies
<properties>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

Basic Usage

Let’s look at a simple example. Consider we have a User entity and a UserDTO:

Domain Models
// User.java
public class User {
private Long id;
private String firstName;
private String lastName;
private String email;
private LocalDate birthDate;
private Address address;
// getters and setters
}
// Address.java
public class Address {
private String street;
private String city;
private String country;
// getters and setters
}
// UserDTO.java
public class UserDTO {
private Long id;
private String fullName; // combination of firstName and lastName
private String email;
private Integer age; // calculated from birthDate
private String location; // combination of city and country
// getters and setters
}

To create a mapper for these classes, we define a mapper interface:

Basic User Mapper
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
@Mapping(target = "age", expression = "java(calculateAge(user.getBirthDate()))")
@Mapping(target = "location", expression = "java(user.getAddress().getCity() + \", \" + user.getAddress().getCountry())")
UserDTO userToUserDTO(User user);
default Integer calculateAge(LocalDate birthDate) {
return Period.between(birthDate, LocalDate.now()).getYears();
}
}

Advanced Features

Custom Method Mappings

Sometimes you need to implement custom mapping logic. MapStruct allows you to define default methods in your mapper interface:

Complex Mapper Example
@Mapper(componentModel = "spring")
public interface ComplexMapper {
@Mapping(target = "status", source = "orderStatus")
@Mapping(target = "totalAmount", expression = "java(calculateTotal(order))")
OrderDTO orderToOrderDTO(Order order);
default BigDecimal calculateTotal(Order order) {
return order.getItems().stream()
.map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}

Collection Mapping

MapStruct can automatically handle collections:

Collection Mapping Example
@Mapper(componentModel = "spring")
public interface OrderMapper {
OrderDTO orderToOrderDTO(Order order);
List<OrderDTO> ordersToOrderDTOs(List<Order> orders);
Set<OrderDTO> ordersToOrderDTOs(Set<Order> orders);
}

Multiple Source Objects

You can map from multiple source objects:

Multiple Source Objects Example
@Mapper(componentModel = "spring")
public interface UserProfileMapper {
@Mapping(source = "user.id", target = "userId")
@Mapping(source = "profile.bio", target = "biography")
@Mapping(source = "settings.theme", target = "userTheme")
UserProfileDTO userAndProfileToDTO(User user, Profile profile, UserSettings settings);
}

Best Practices

  1. Use Spring Component Model: Always use @Mapper(componentModel = "spring") when working with Spring applications to enable dependency injection.

  2. Null Value Handling: Configure null value handling at the mapper level:

Null Value Handling Configuration
@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
  1. Documentation: Document complex mappings using @Mapping annotation’s defaultValue and defaultExpression:
@Mapping(target = "status",
source = "orderStatus",
defaultValue = "PENDING",
defaultExpression = "java(OrderStatus.PENDING)")

Testing

Testing MapStruct mappings is straightforward:

Mapper Test Example
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void shouldMapUserToUserDTO() {
// Given
User user = new User();
user.setFirstName("John");
user.setLastName("Doe");
user.setEmail("john@example.com");
user.setBirthDate(LocalDate.of(1990, 1, 1));
Address address = new Address();
address.setCity("New York");
address.setCountry("USA");
user.setAddress(address);
// When
UserDTO userDTO = userMapper.userToUserDTO(user);
// Then
assertThat(userDTO.getFullName()).isEqualTo("John Doe");
assertThat(userDTO.getEmail()).isEqualTo("john@example.com");
assertThat(userDTO.getLocation()).isEqualTo("New York, USA");
assertThat(userDTO.getAge()).isEqualTo(33);
}
}

Complex Object Conversion Example

Let’s look at a more complex example involving an e-commerce order system:

Order.java - Main Order Entity
public class Order {
private Long id;
private Customer customer;
private List<OrderItem> items;
private PaymentDetails payment;
private OrderStatus status;
private LocalDateTime createdAt;
private ShippingAddress shippingAddress;
// getters and setters
}
Customer.java - Customer Entity
public class Customer {
private Long id;
private String firstName;
private String lastName;
private String email;
private CustomerType type;
private List<Address> addresses;
// getters and setters
}
OrderItem.java - Order Item Entity
public class OrderItem {
private Product product;
private Integer quantity;
private BigDecimal priceAtOrder;
private List<String> customizations;
// getters and setters
}
PaymentDetails.java - Payment Entity
public class PaymentDetails {
private PaymentMethod method;
private PaymentStatus status;
private BigDecimal amount;
private String transactionId;
// getters and setters
}
OrderSummaryDTO.java - Order Summary DTO
public class OrderSummaryDTO {
private String orderNumber;
private String customerFullName;
private String customerEmail;
private List<OrderItemDTO> items;
private BigDecimal totalAmount;
private String status;
private String paymentStatus;
private String shippingAddress;
private LocalDateTime orderDate;
// getters and setters
}
OrderItemDTO.java - Order Item DTO
public class OrderItemDTO {
private String productName;
private String productSku;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal totalPrice;
private List<String> customizations;
// getters and setters
}
OrderMapper.java - Complex Order Mapper
@Mapper(componentModel = "spring", imports = {BigDecimal.class})
public interface OrderMapper {
@Mapping(target = "orderNumber", expression = "java(generateOrderNumber(order))")
@Mapping(target = "customerFullName",
expression = "java(order.getCustomer().getFirstName() + \" \" + order.getCustomer().getLastName())")
@Mapping(target = "customerEmail", source = "customer.email")
@Mapping(target = "totalAmount", expression = "java(calculateTotalAmount(order))")
@Mapping(target = "status", expression = "java(order.getStatus().name())")
@Mapping(target = "paymentStatus", source = "payment.status")
@Mapping(target = "shippingAddress", expression = "java(formatAddress(order.getShippingAddress()))")
@Mapping(target = "orderDate", source = "createdAt")
OrderSummaryDTO orderToOrderSummaryDTO(Order order);
@Mapping(target = "productName", source = "product.name")
@Mapping(target = "productSku", source = "product.sku")
@Mapping(target = "unitPrice", source = "priceAtOrder")
@Mapping(target = "totalPrice",
expression = "java(item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity())))")
OrderItemDTO orderItemToDTO(OrderItem item);
default String generateOrderNumber(Order order) {
return String.format("ORD-%d-%tY%<tm%<td",
order.getId(), order.getCreatedAt());
}
default BigDecimal calculateTotalAmount(Order order) {
return order.getItems().stream()
.map(item -> item.getPriceAtOrder()
.multiply(new BigDecimal(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
default String formatAddress(ShippingAddress address) {
return String.format("%s, %s, %s, %s - %s",
address.getStreet(),
address.getCity(),
address.getState(),
address.getCountry(),
address.getZipCode());
}
@AfterMapping
default void handleSpecialCustomerTypes(Order order, @MappingTarget OrderSummaryDTO dto) {
if (order.getCustomer().getType() == CustomerType.VIP) {
dto.setCustomerFullName(dto.getCustomerFullName() + " (VIP)");
}
}
}
OrderService.java - Service Usage Example
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
public OrderSummaryDTO getOrderSummary(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
return orderMapper.orderToOrderSummaryDTO(order);
}
}

This example demonstrates several advanced MapStruct features:

  1. Complex Object Hierarchy: Mapping nested objects (Order → Customer → Address)
  2. Collection Mapping: Handling lists of OrderItems
  3. Custom Methods: Using helper methods for complex transformations
  4. After Mapping: Post-processing with @AfterMapping
  5. Expression Mapping: Using Java expressions for computed fields
  6. Multiple Source Fields: Combining multiple fields into one
  7. Format Transformation: Converting complex objects to formatted strings

The mapper handles:

  • Nested object traversal
  • Collection transformation
  • Custom business logic
  • Formatted string output
  • Calculated fields
  • Special case handling

This example shows how MapStruct can handle complex real-world scenarios while keeping the code clean and maintainable.

Security Considerations and Known Vulnerabilities

As of February 2024, MapStruct has maintained a strong security track record. However, it’s important to note:

MapStruct Core (1.5.5.Final)

  • Currently no known critical vulnerabilities
  • The library itself performs compile-time code generation, minimizing runtime security risks
  • No direct exposure to user input or network operations

When using MapStruct with Spring Boot, be aware of these dependencies:

  1. maven-compiler-plugin (3.8.1)

    • No known critical vulnerabilities
    • Recommended to use version 3.11.0 or later for improved security
  2. Spring Framework Integration

    • MapStruct’s Spring integration is passive and inherits Spring’s security context
    • Follow Spring Security best practices when exposing mapped objects through APIs

Best Security Practices

  1. Input Validation: Always validate input before mapping
Security Example - Input Validation
@Mapper(componentModel = "spring")
public interface UserMapper {
default UserDTO toDto(User user) {
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}
// perform mapping
}
}
  1. Sensitive Data: Be careful when mapping sensitive information
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "password", ignore = true)
@Mapping(target = "securityQuestions", ignore = true)
UserDTO userToUserDTO(User user);
}
  1. Regular Updates: Keep dependencies updated to receive security patches

Conclusion

MapStruct is a powerful tool that simplifies object mapping in Java applications. It provides:

  • Type-safe mapping
  • Compile-time error checking
  • High performance
  • Clean and maintainable code
  • Excellent integration with Spring Framework

By using MapStruct, you can focus on your business logic while letting the library handle the tedious task of object transformation. The generated code is easy to debug and performs better than reflection-based mapping frameworks.

Alternative Libraries

While MapStruct is an excellent choice for object mapping, there are several alternatives worth considering:

ModelMapper

  • Pros:
    • Runtime mapping using reflection
    • No code generation required
    • Flexible mapping configurations
    • Intuitive API
  • Cons:
    • Lower performance due to reflection
    • No compile-time type safety
    • Higher memory usage
ModelMapper Example
ModelMapper modelMapper = new ModelMapper();
UserDTO userDTO = modelMapper.map(user, UserDTO.class);

JMapper

  • Pros:
    • High performance
    • XML or annotation configuration
    • Supports complex mappings
  • Cons:
    • Less active community
    • Limited documentation
    • Steeper learning curve

Dozer

  • Pros:
    • Rich feature set
    • XML or API configuration
    • Good for legacy systems
  • Cons:
    • Slower performance
    • Heavy memory footprint
    • XML configuration can be verbose

Note: The ratings are relative comparisons based on available documentation, community feedback, and published benchmarks. Your specific use case may yield different results.

When to Choose Each

  1. Choose MapStruct when:

    • Performance is critical
    • Compile-time type safety is required
    • Working with Spring Boot applications
    • Need clean, maintainable generated code
  2. Choose ModelMapper when:

    • Quick prototyping is needed
    • Runtime mapping configuration is required
    • Type safety is less critical
    • Simple setup is preferred
  3. Choose JMapper when:

    • High performance is needed
    • XML configuration is preferred
    • Working with legacy systems
  4. Choose Dozer when:

    • Complex legacy system integration
    • Extensive mapping configurations needed
    • Performance is not critical

Each library has its strengths, but MapStruct’s combination of performance, type safety, and excellent Spring integration makes it the preferred choice for modern Java applications.

References