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
- Use Component Model: Prefer
componentModel = "spring"for Spring applications - Central Configuration: Use
@MapperConfigfor shared settings - Custom Methods: Use
@Namedfor complex transformations - Testing: Always test your mappers
- Error Handling: Handle null values gracefully
- Performance: MapStruct generates efficient code, but avoid complex expressions in mappings
- Documentation: Use
@Mappingannotations 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.