MapStruct for Object Mapping in Java: A Complete Guide

MapStruct is a code generator that simplifies object mapping in Java by automatically generating mapping code at compile time. It eliminates boilerplate code and provides type-safe, fast, and maintainable object transformation.


Why MapStruct?

Advantages over Manual Mapping:

  • Compile-time Safety: Errors caught during compilation
  • Performance: No reflection, generated code is as fast as hand-written
  • Type Safety: Compile-time verification of mapping correctness
  • Maintainability: Easy to refactor and update
  • Less Boilerplate: No manual getter/setter code
  • IDE Support: Full IDE integration with code completion

Setup and Dependencies

Maven Dependencies

<properties>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
<dependencies>
<!-- MapStruct Core -->
<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.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

Gradle Setup

plugins {
id 'java'
}
dependencies {
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
}

Basic Mapping Examples

Example 1: Simple Object Mapping

Source and Target Classes:

// Source class
public class User {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;
private LocalDateTime createdAt;
// Constructors
public User() {}
public User(Long id, String username, String email, String firstName, 
String lastName, LocalDateTime createdAt) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
this.createdAt = createdAt;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
// Target class
public class UserDTO {
private Long id;
private String username;
private String email;
private String fullName;
private String createdAt;
// Constructors, getters, and setters
public UserDTO() {}
public UserDTO(Long id, String username, String email, String fullName, String createdAt) {
this.id = id;
this.username = username;
this.email = email;
this.fullName = fullName;
this.createdAt = createdAt;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
}

Mapper Interface:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(target = "fullName", expression = "java(user.getFirstName() + ' ' + user.getLastName())")
@Mapping(target = "createdAt", source = "createdAt", dateFormat = "yyyy-MM-dd HH:mm:ss")
UserDTO userToUserDTO(User user);
@Mapping(target = "firstName", expression = "java(extractFirstName(userDTO.getFullName()))")
@Mapping(target = "lastName", expression = "java(extractLastName(userDTO.getFullName()))")
@Mapping(target = "createdAt", ignore = true) // We'll handle this separately
User userDTOToUser(UserDTO userDTO);
// Default methods for custom logic
default String extractFirstName(String fullName) {
if (fullName == null || fullName.trim().isEmpty()) {
return null;
}
String[] names = fullName.split(" ");
return names.length > 0 ? names[0] : null;
}
default String extractLastName(String fullName) {
if (fullName == null || fullName.trim().isEmpty()) {
return null;
}
String[] names = fullName.split(" ");
return names.length > 1 ? names[1] : null;
}
}

Usage Example:

public class BasicMappingDemo {
public static void main(String[] args) {
// Create source object
User user = new User(
1L, 
"johndoe", 
"[email protected]", 
"John", 
"Doe", 
LocalDateTime.now()
);
// Map to DTO
UserDTO userDTO = UserMapper.INSTANCE.userToUserDTO(user);
System.out.println("Original User: " + user.getFirstName() + " " + user.getLastName());
System.out.println("Mapped UserDTO: " + userDTO.getFullName());
System.out.println("Email: " + userDTO.getEmail());
System.out.println("Created At: " + userDTO.getCreatedAt());
// Map back to User
User mappedUser = UserMapper.INSTANCE.userDTOToUser(userDTO);
System.out.println("Mapped back - First Name: " + mappedUser.getFirstName());
System.out.println("Mapped back - Last Name: " + mappedUser.getLastName());
}
}

Advanced Mapping Features

Example 2: Complex Object Mapping with Nested Objects

Nested Object Classes:

// Address classes
public class Address {
private String street;
private String city;
private String state;
private String zipCode;
private String country;
// Constructors, getters, and setters
public Address() {}
public Address(String street, String city, String state, String zipCode, String country) {
this.street = street;
this.city = city;
this.state = state;
this.zipCode = zipCode;
this.country = country;
}
// Getters and setters...
}
public class AddressDTO {
private String fullAddress;
private String location;
// Constructors, getters, and setters
public AddressDTO() {}
public AddressDTO(String fullAddress, String location) {
this.fullAddress = fullAddress;
this.location = location;
}
// Getters and setters...
}
// User with Address
public class UserWithAddress {
private Long id;
private String username;
private String email;
private Address address;
// Constructors, getters, and setters
public UserWithAddress() {}
public UserWithAddress(Long id, String username, String email, Address address) {
this.id = id;
this.username = username;
this.email = email;
this.address = address;
}
// Getters and setters...
}
public class UserWithAddressDTO {
private Long id;
private String username;
private String email;
private AddressDTO address;
// Constructors, getters, and setters...
}

Complex Mapper:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
@Mapper
public interface ComplexUserMapper {
ComplexUserMapper INSTANCE = Mappers.getMapper(ComplexUserMapper.class);
@Mapping(target = "address", source = "address", qualifiedByName = "addressToAddressDTO")
UserWithAddressDTO userWithAddressToDTO(UserWithAddress user);
@Mapping(target = "address", source = "address", qualifiedByName = "addressDTOToAddress")
UserWithAddress dtoToUserWithAddress(UserWithAddressDTO dto);
@Named("addressToAddressDTO")
default AddressDTO addressToAddressDTO(Address address) {
if (address == null) {
return null;
}
String fullAddress = String.format("%s, %s, %s %s", 
address.getStreet(), address.getCity(), address.getState(), address.getZipCode());
String location = String.format("%s, %s", address.getCity(), address.getCountry());
return new AddressDTO(fullAddress, location);
}
@Named("addressDTOToAddress")
default Address addressDTOToAddress(AddressDTO addressDTO) {
if (addressDTO == null || addressDTO.getFullAddress() == null) {
return null;
}
// Simple parsing logic - in real scenario, use proper parsing
String[] parts = addressDTO.getFullAddress().split(", ");
return new Address(
parts.length > 0 ? parts[0] : null, // street
parts.length > 1 ? parts[1] : null, // city
parts.length > 2 ? parts[2] : null, // state
parts.length > 3 ? parts[3] : null, // zipCode
addressDTO.getLocation() != null ? 
addressDTO.getLocation().split(", ")[1] : null // country
);
}
}

Example 3: Collection Mapping

Collection Mapper:

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Set;
@Mapper
public interface CollectionMapper {
CollectionMapper INSTANCE = Mappers.getMapper(CollectionMapper.class);
// List to List
List<UserDTO> usersToUserDTOs(List<User> users);
// Set to Set
Set<UserDTO> usersToUserDTOs(Set<User> users);
// List to Set
Set<UserDTO> usersToUserDTOsSet(List<User> users);
}

Usage:

public class CollectionMappingDemo {
public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "user1", "[email protected]", "John", "Doe", LocalDateTime.now()),
new User(2L, "user2", "[email protected]", "Jane", "Smith", LocalDateTime.now()),
new User(3L, "user3", "[email protected]", "Bob", "Johnson", LocalDateTime.now())
);
List<UserDTO> userDTOs = CollectionMapper.INSTANCE.usersToUserDTOs(users);
System.out.println("Mapped " + userDTOs.size() + " users to DTOs");
userDTOs.forEach(dto -> 
System.out.println(" - " + dto.getFullName() + " (" + dto.getEmail() + ")")
);
}
}

Spring Integration

Example 4: MapStruct with Spring Boot

Spring-based Mapper:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.springframework.stereotype.Component;
@Mapper(componentModel = "spring")
public interface SpringUserMapper {
@Mapping(target = "fullName", expression = "java(user.getFirstName() + ' ' + user.getLastName())")
@Mapping(target = "createdAt", source = "createdAt", dateFormat = "yyyy-MM-dd HH:mm:ss")
UserDTO userToUserDTO(User user);
User userDTOToUser(UserDTO userDTO);
List<UserDTO> usersToUserDTOs(List<User> users);
}

Service Class:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final SpringUserMapper userMapper;
@Autowired
public UserService(SpringUserMapper userMapper) {
this.userMapper = userMapper;
}
public UserDTO getUserDTO(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
return userMapper.userToUserDTO(user);
}
public List<UserDTO> getAllActiveUsers() {
List<User> activeUsers = userRepository.findByActiveTrue();
return userMapper.usersToUserDTOs(activeUsers);
}
public User createUser(UserDTO userDTO) {
User user = userMapper.userDTOToUser(userDTO);
user.setCreatedAt(LocalDateTime.now());
user.setActive(true);
return userRepository.save(user);
}
}

Advanced Configuration

Example 5: Custom Mapper with Configuration

Mapper Configuration:

import org.mapstruct.MapperConfig;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
@MapperConfig(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE,
unmappedSourcePolicy = ReportingPolicy.WARN
)
public interface CentralConfig {
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
User toEntity(UserDTO dto);
}

Inheriting Mapper:

@Mapper(config = CentralConfig.class)
public interface ConfiguredUserMapper extends CentralConfig {
// Inherits configuration from CentralConfig
@Mapping(target = "fullName", expression = "java(user.getFirstName() + ' ' + user.getLastName())")
UserDTO toDTO(User user);
// Uses configuration from CentralConfig for toEntity
User toEntity(UserDTO dto);
}

Example 6: Conditional Mapping

Conditional Mapper:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Condition;
@Mapper
public interface ConditionalMapper {
@Mapping(target = "email", source = "email", condition = "isValidEmail")
UserDTO userToUserDTO(User user);
@Condition
default boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.contains(".");
}
@Mapping(target = "username", source = "username", defaultExpression = "java(generateUsername())")
User userDTOToUser(UserDTO dto);
default String generateUsername() {
return "user_" + System.currentTimeMillis();
}
}

Real-World Use Case

Example 7: E-commerce Domain Mapping

Domain Classes:

// Domain classes
public class Order {
private Long id;
private String orderNumber;
private Customer customer;
private List<OrderItem> items;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime orderDate;
// Constructors, getters, setters
}
public class OrderItem {
private Long id;
private Product product;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal totalPrice;
// Constructors, getters, setters
}
public class Customer {
private Long id;
private String name;
private String email;
private Address address;
// Constructors, getters, setters
}
public class Product {
private Long id;
private String name;
private String description;
private BigDecimal price;
private String category;
// Constructors, getters, setters
}
// DTO classes
public class OrderDTO {
private String orderNumber;
private String customerName;
private String customerEmail;
private String shippingAddress;
private List<OrderItemDTO> items;
private String formattedTotal;
private String status;
private String orderDate;
// Constructors, getters, setters
}
public class OrderItemDTO {
private String productName;
private Integer quantity;
private String formattedUnitPrice;
private String formattedTotalPrice;
// Constructors, getters, setters
}

E-commerce Mapper:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.Locale;
@Mapper
public interface OrderMapper {
OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
@Mapping(target = "customerName", source = "customer.name")
@Mapping(target = "customerEmail", source = "customer.email")
@Mapping(target = "shippingAddress", source = "customer.address", qualifiedByName = "formatAddress")
@Mapping(target = "formattedTotal", source = "totalAmount", qualifiedByName = "formatCurrency")
@Mapping(target = "status", source = "status", qualifiedByName = "formatStatus")
@Mapping(target = "orderDate", source = "orderDate", dateFormat = "MMMM dd, yyyy 'at' HH:mm")
@Mapping(target = "items", source = "items")
OrderDTO orderToOrderDTO(Order order);
@Mapping(target = "productName", source = "product.name")
@Mapping(target = "formattedUnitPrice", source = "unitPrice", qualifiedByName = "formatCurrency")
@Mapping(target = "formattedTotalPrice", source = "totalPrice", qualifiedByName = "formatCurrency")
OrderItemDTO orderItemToOrderItemDTO(OrderItem orderItem);
@Named("formatAddress")
default String formatAddress(Address address) {
if (address == null) {
return "No address provided";
}
return String.format("%s, %s, %s %s, %s", 
address.getStreet(), address.getCity(), 
address.getState(), address.getZipCode(), address.getCountry());
}
@Named("formatCurrency")
default String formatCurrency(BigDecimal amount) {
if (amount == null) {
return "$0.00";
}
return NumberFormat.getCurrencyInstance(Locale.US).format(amount);
}
@Named("formatStatus")
default String formatStatus(OrderStatus status) {
if (status == null) {
return "UNKNOWN";
}
return status.name().charAt(0) + status.name().substring(1).toLowerCase();
}
}

Testing MapStruct Mappers

Example 8: Unit Testing

import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class UserMapperTest {
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
@Test
void testUserToUserDTO() {
// Given
User user = new User(1L, "johndoe", "[email protected]", 
"John", "Doe", LocalDateTime.now());
// When
UserDTO userDTO = userMapper.userToUserDTO(user);
// Then
assertNotNull(userDTO);
assertEquals(user.getId(), userDTO.getId());
assertEquals(user.getUsername(), userDTO.getUsername());
assertEquals(user.getEmail(), userDTO.getEmail());
assertEquals("John Doe", userDTO.getFullName());
assertNotNull(userDTO.getCreatedAt());
}
@Test
void testUserDTOToUser() {
// Given
UserDTO userDTO = new UserDTO(1L, "johndoe", "[email protected]", 
"John Doe", "2024-01-15 10:30:00");
// When
User user = userMapper.userDTOToUser(userDTO);
// Then
assertNotNull(user);
assertEquals(userDTO.getId(), user.getId());
assertEquals(userDTO.getUsername(), user.getUsername());
assertEquals(userDTO.getEmail(), user.getEmail());
assertEquals("John", user.getFirstName());
assertEquals("Doe", user.getLastName());
}
@Test
void testUsersToUserDTOs() {
// Given
List<User> users = List.of(
new User(1L, "user1", "[email protected]", "John", "Doe", LocalDateTime.now()),
new User(2L, "user2", "[email protected]", "Jane", "Smith", LocalDateTime.now())
);
// When
List<UserDTO> userDTOs = userMapper.usersToUserDTOs(users);
// Then
assertNotNull(userDTOs);
assertEquals(2, userDTOs.size());
assertEquals("John Doe", userDTOs.get(0).getFullName());
assertEquals("Jane Smith", userDTOs.get(1).getFullName());
}
}

Best Practices

  1. Use Component Model: Prefer componentModel = "spring" for Spring applications
  2. Central Configuration: Use @MapperConfig for shared settings
  3. Custom Methods: Use @Named for complex transformations
  4. Testing: Always test your mappers
  5. Error Handling: Handle null values gracefully
  6. Performance: MapStruct generates efficient code, but avoid complex expressions in mappings
  7. Documentation: Use @Mapping annotations to document mapping rules

Conclusion

MapStruct provides a powerful, type-safe, and efficient solution for object mapping in Java:

Key Benefits:

  • Zero Runtime Overhead: Compile-time code generation
  • Type Safety: Compile-time error detection
  • Performance: As fast as hand-written code
  • Maintainability: Easy to refactor and understand
  • Flexibility: Custom methods and complex mappings
  • Spring Integration: Seamless Spring Boot integration

When to Use MapStruct:

  • Complex object transformations
  • Microservices with multiple DTOs
  • API development with request/response mapping
  • Data migration and transformation
  • Any project requiring object-to-object mapping

MapStruct eliminates the boilerplate of manual mapping while providing the performance and type safety that makes it the preferred choice for object mapping in modern Java applications.


Next Steps: Explore MapStruct's advanced features like decorators, inheritance, and custom mappers for complex mapping scenarios. Integrate with your build process and consider using it alongside other mapping libraries for specific use cases.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper