Applying Conditional Mapping Using MapStruct
MapStruct is a handy tool for Java that helps make it easier to transfer information between different types of Java objects. It creates code during compilation that efficiently handles the mapping process. Mapstruct lets developers set rules for conditional mapping of attributes between Java bean types. In this article, we’ll explore how to use conditional mapping with MapStruct.
1. Brief Overview of MapStruct
MapStruct is a code generation library for Java that simplifies the implementation of mappings between Java bean types. It creates code for mapping, so we don’t have to write a bunch of repetitive code to convert data between different types of objects. MapStruct provides a straightforward, type-safe, and efficient way to handle object mapping.
1.1 Key features of MapStruct
Key features of MapStruct include:
- Annotation-Based Mapping: MapStruct relies on annotations to generate mapping code. Developers can use annotations like
@Mapperto mark interfaces as mapping interfaces and@Mappingto customize the mapping behavior for specific fields. - Type-Safe Mappings: The library generates type-safe mapping code, reducing the chances of runtime errors related to incompatible types. The code it generates relies on type information during compilation to ensure accuracy.
- Customization: We can customize the mapping behavior by providing our own methods or implementations for specific mappings. This lets us have detailed control over how things are converted when we need it.
- Null Value Handling: MapStruct offers options for handling null values during mapping, giving us the power to decide whether to keep and pass along null values or use default values instead.
- Support for Different Mapping Strategies: MapStruct supports various mapping strategies, such as method-based, constructor-based, or field-based mappings. This flexibility allows us to choose the most suitable approach based on our specific requirements.
1.2 Real-World Use Cases
- DTO (Data Transfer Object) Mapping:
- Use Case: Transforming data between entities and DTOs.
- Example: Mapping data from a
Userentity to aUserDTOfor sending user information over a REST API.
- Entity to View Model Mapping:
- Use Case: Converting data from database entities to view models for presentation.
- Example: Mapping a
ProductEntityto aProductViewModelfor displaying product information in a web application.
- Conditional Mapping:
- Use Case: Applying specific mapping rules based on conditions.
- Example: Mapping an order entity to an order DTO, but excluding certain items from the DTO if they are marked as confidential.
- Enum Conversion:
- Use Case: Converting between different enum types.
- Example: Mapping a
Statusenum in a domain object to aStringrepresentation in a DTO, or vice versa, based on specific business logic.
2. Getting Started with MapStruct
Setting up MapStruct in our Java project is a straightforward process that involves adding the library and its annotation processor as dependencies.
2.1 MapStruct Maven Dependency SetUp
In our Maven project, we can add the MapStruct dependency to the <dependencies> section of our pom.xml file like this:
<dependencies>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version> <!-- Use the latest version -->
</dependency>
<!-- MapStruct Annotation Processor (for compilation) -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version> <!-- Use the same version as the mapstruct dependency -->
<scope>provided</scope>
</dependency>
</dependencies>
2.2 Basic Mapping with MapStruct
Let’s explore how to get started with MapStruct with a very basic mapping example. DTO mapping is a common scenario where data needs to be transferred between entities and Data Transfer Objects (DTOs).
In this example, we will define a UserMapper interface that generates the mapping code for converting a User object to a UserDTO. The code below shows the User and UserDTO classes:
public class User {
private String username;
private String email;
public User(String username, String email) {
this.username = username;
this.email = email;
}
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 class UserDTO {
private String username;
private String email;
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;
}
}
To establish a mapper between the two classes, we create an interface called UserMapper and annotate it with the @Mapper annotation. This annotation signals MapStruct to automatically recognize the need for generating a mapper implementation between the specified objects.
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserDTO userToUserDTO(User user);
}
Here’s a breakdown of the code above:
@Mapper: This annotation signifies that the interfaceUserMapperis a MapStruct mapper. MapStruct will analyze this interface and generate the corresponding implementation at compile-time.UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);: This line creates a constant instance of theUserMapperinterface.Mappers.getMapper(UserMapper.class)initializes and returns an implementation of theUserMapperinterface.UserDTO userToUserDTO(User user);: This method defines the mapping from aUserobject to aUserDTOobject. MapStruct will automatically generate the implementation for this method based on the fields in theUserandUserDTOclasses.
When we compile the application, the MapStruct annotation processor plugin will pick the UserMapper interface and create an implementation for it which would look like this:
package com.jcg.basicmapping;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-12-21T10:13:51+0100",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 17.0.9 (Oracle Corporation)"
)
public class UserMapperImpl implements UserMapper {
@Override
public UserDTO userToUserDTO(User user) {
if ( user == null ) {
return null;
}
UserDTO userDTO = new UserDTO();
userDTO.setUsername( user.getUsername() );
userDTO.setEmail( user.getEmail() );
return userDTO;
}
}
To use this mapper, we can call the userToUserDTO method on the INSTANCE constant. For example:
public class BasicMapping {
public static void main(String[] args) {
// Creating a sample User object
User user = new User();
user.setUsername("John Fish");
user.setEmail("john.fish@jcg.com");
// Using the UserMapper to map User to UserDTO
UserDTO userDTO = UserMapper.INSTANCE.userToUserDTO(user);
// Displaying the mapped result
System.out.println("Mapped UserDTO:");
System.out.println("Username: " + userDTO.getUsername());
System.out.println("Email: " + userDTO.getEmail());
}
}
In this example, we create a User object, and the userToUserDTO method from the UserMapper interface is used to map it to a UserDTO object. The mapped UserDTO object is then printed to the console.
3. Understanding Conditional Mapping
Conditional mapping in MapStruct enables us to define rules that guide the mapping process based on certain conditions. We can use annotations and custom methods to establish conditions and map objects accordingly. For example, we might want to exclude certain fields and apply transformations only when specific conditions are met, or map objects differently based on their state.
Conditional mapping is particularly useful when we need to customize the mapping behavior depending on certain criteria or business logic.
3.1 Conditional Mapping Example
Let’s consider a scenario where we have an Order class and we want to map it to an OrderDTO class. However, we want to exclude items marked as confidential in the mapping process.
public class Item {
private String name;
private boolean confidential;
public Item() {
}
public Item(String name, boolean confidential) {
this.name = name;
this.confidential = confidential;
}
// getters and setters
}
public class ItemDTO {
private String name;
public ItemDTO() {
}
// getters and setters
}
public class Order {
private List items;
public Order() {
}
public List getItems() {
return items;
}
public void setItems(List items) {
this.items = items;
}
}
public class OrderDTO {
private List items;
public OrderDTO() {
}
public List getItems() {
return items;
}
public void setItems(List items) {
this.items = items;
}
}
3.2 Conditional Mapping Interface
In this example, the OrderMapper interface maps orders to order DTOs, excluding confidential items.
@Mapper
public interface OrderMapper {
OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
@Mappings({
@Mapping(target = "items", source = "items", qualifiedByName = "nonConfidentialItems")
})
OrderDTO orderToOrderDTO(Order order);
@Named("nonConfidentialItems")
default List mapNonConfidentialItems(List items) {
return items.stream()
.filter(item -> !item.isConfidential())
.map(this::itemToItemDTO)
.collect(Collectors.toList());
}
ItemDTO itemToItemDTO(Item item);
}
In the OrderMapper interface:
orderToOrderDTO: This method maps theOrderclass to theOrderDTOclass. The@Mappingsannotation specifies that theitemsfield should be mapped using the nonConfidentialItems method.mapNonConfidentialItems: This method is qualified by name as nonConfidentialItems. It filters out items marked as confidential and maps the rest toItemDTOobjects.
3.3 Conditional Mapping in Action
public class ConditionalMappingExample {
public static void main(String[] args) {
// Creating a sample Order with items, some marked as confidential
Order order = new Order();
List<Item> items = List.of(
new Item("Gullivers Travels", false),
new Item("Age of Reason", true),
new Item("Things Fall Apart", false)
);
order.setItems(items);
// Using OrderMapper to map Order to OrderDTO
OrderDTO orderDTO = OrderMapper.INSTANCE.orderToOrderDTO(order);
// Displaying the mapped result
System.out.println("Mapped OrderDTO:");
for (ItemDTO itemDTO : orderDTO.getItems()) {
System.out.println("Item: " + itemDTO.getName());
}
}
}
In this ConditionalMappingExample class, we create a sample Order object with items, some marked as confidential. We then use the OrderMapper interface to map this Order to an OrderDTO. The resulting OrderDTO is printed to the console, with the results showing that confidential items are excluded from the mapping.
4. Conclusion
In this article, we explored a simple approach for conditional mapping of attributes between Java bean types using MapStruct. Whenever we need to map fields conditionally, conditional mapping in MapStruct provides a flexible way to control the mapping process based on specific conditions.
5. Download the Source Code
This was an example of applying conditional mapping of attributes between Java bean types using MapStruct.
You can download the full source code of this example here: Java Mapstruct Bean Types Conditional


