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
@Mapper
to mark interfaces as mapping interfaces and@Mapping
to 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
User
entity to aUserDTO
for 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
ProductEntity
to aProductViewModel
for 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
Status
enum in a domain object to aString
representation 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 interfaceUserMapper
is 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 theUserMapper
interface.Mappers.getMapper(UserMapper.class)
initializes and returns an implementation of theUserMapper
interface.UserDTO userToUserDTO(User user);
: This method defines the mapping from aUser
object to aUserDTO
object. MapStruct will automatically generate the implementation for this method based on the fields in theUser
andUserDTO
classes.
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 theOrder
class to theOrderDTO
class. The@Mappings
annotation specifies that theitems
field 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 toItemDTO
objects.
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