Core Java

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 a UserDTO 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 a ProductViewModel 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 a String 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 interface UserMapper 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 the UserMapper interface. Mappers.getMapper(UserMapper.class) initializes and returns an implementation of the UserMapper interface.
  • UserDTO userToUserDTO(User user);: This method defines the mapping from a User object to a UserDTO object. MapStruct will automatically generate the implementation for this method based on the fields in the User and UserDTO 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.

Fig 1: Basic Java Mapstruct example
Fig 1: Basic Java Mapstruct example

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 the Order class to the OrderDTO class. The @Mappings annotation specifies that the items 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 to ItemDTO 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.

Fig 2: Java Mapstruct conditional mapping example output
Fig 2: Java Mapstruct conditional mapping example output

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.

Download
You can download the full source code of this example here: Java Mapstruct Bean Types Conditional

Omozegie Aziegbe

Omos holds a Master degree in Information Engineering with Network Management from the Robert Gordon University, Aberdeen. Omos is currently a freelance web/application developer who is currently focused on developing Java enterprise applications with the Jakarta EE framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button