Guide to MapStruct Library in Java

Introduction to MapStruct

MapStruct is a Java annotation processor that generates type-safe and efficient mappers for object mappings. It is widely used for mapping data between different Java beans, typically in the context of transferring data between layers in an application. MapStruct generates mapping code at compile time, ensuring high performance and type safety.

How MapStruct Works

MapStruct generates implementation code for mapping interfaces at compile time. You define mapping methods in an interface, and MapStruct generates the implementation for you. This approach avoids the runtime overhead of reflection and makes the mapping code type safe and fast.

Here are five key points on how MapStruct works:

  1. Annotation Processor:

    • MapStruct uses an annotation processor to generate mapping code at compile time. This means that the mapping logic is generated as Java source code, ensuring high performance and type safety.
    • The core annotation used is @Mapper, which is applied to an interface or abstract class that defines the mapping methods.
  2. Automatic Mapping:

    • MapStruct automatically maps properties with the same name and type between source and target objects. This reduces the need for manual mapping logic and makes it easy to use.
    • For example, if you have two classes PersonDTO and Person with properties firstName, lastName, and age, MapStruct will automatically map these properties if they have the same names and types.
  3. Custom Mappings and Conversions:

    • MapStruct allows you to define custom mappings using the @Mapping annotation. You can specify the source and target properties explicitly if they have different names.
    • It also supports custom-type conversions using methods annotated with @Named. You can define methods to handle specific type conversions and reference them in your mapping methods.
  4. Nested Mappings:

    • MapStruct supports nested mappings, which means you can map nested objects and their properties. This is particularly useful for complex object graphs.
    • For example, you can map nested objects like Address inside an Employee object by specifying the nested property paths in the @Mapping annotation.
  5. Mapping Collections:

    • MapStruct can handle mapping collections such as lists and sets. It can map collections of objects from the source to the target, provided the individual elements can be mapped.
    • This is useful when dealing with relationships like one-to-many or many-to-many between objects in the source and target classes.

Installation

Adding MapStruct to Your Project

To use MapStruct, add the following dependencies to your pom.xml if you're using Maven:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version> <!-- or the latest version -->
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.2.Final</version>
    <scope>provided</scope>
</dependency>

For Gradle:

implementation 'org.mapstruct:mapstruct:1.5.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.2.Final'

Basic Usage: Simple Bean Mapping

Let's start with a simple example of mapping between two Java beans, PersonDTO and Person.

Defining the Beans

public class PersonDTO {
    private String firstName;
    private String lastName;
    private int age;

    // Getters and Setters
}
public class Person {
    private String givenName;
    private String familyName;
    private int age;

    // Getters and Setters
}

Creating the Mapper

Create a mapper interface with the @Mapper annotation.

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(source = "firstName", target = "givenName")
    @Mapping(source = "lastName", target = "familyName")
    Person toPerson(PersonDTO personDTO);

    @Mapping(source = "givenName", target = "firstName")
    @Mapping(source = "familyName", target = "lastName")
    PersonDTO toPersonDTO(Person person);
}

Using the Mapper

public class MapStructExample {
    public static void main(String[] args) {
        PersonMapper mapper = PersonMapper.INSTANCE;

        PersonDTO personDTO = new PersonDTO();
        personDTO.setFirstName("Amit");
        personDTO.setLastName("Sharma");
        personDTO.setAge(30);

        Person person = mapper.toPerson(personDTO);

        System.out.println("Person: " + person.getGivenName() + " " + person.getFamilyName() + ", Age: " + person.getAge());

        PersonDTO mappedPersonDTO = mapper.toPersonDTO(person);
        System.out.println("PersonDTO: " + mappedPersonDTO.getFirstName() + " " + mappedPersonDTO.getLastName() + ", Age: " + mappedPersonDTO.getAge());
    }
}

Explanation: This example creates a mapper interface, PersonMapper, to map between PersonDTO and Person. The @Mapping annotations specify the source and target fields for the mapping. The generated mapper is used to convert between the two types.

Output

Person: Amit Sharma, Age: 30
PersonDTO: Amit Sharma, Age: 30

Custom Mappings

You can define custom mappings using methods in the mapper interface.

Defining the Beans

public class EmployeeDTO {
    private String empName;
    private String empId;

    // Getters and Setters
}
public class Employee {
    private String name;
    private String id;

    // Getters and Setters
}

Creating the Mapper

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper
public interface EmployeeMapper {
    EmployeeMapper INSTANCE = Mappers.getMapper(EmployeeMapper.class);

    @Mapping(source = "empName", target = "name")
    @Mapping(source = "empId", target = "id")
    Employee toEmployee(EmployeeDTO employeeDTO);

    @Mapping(source = "name", target = "empName")
    @Mapping(source = "id", target = "empId")
    EmployeeDTO toEmployeeDTO(Employee employee);
}

Using the Mapper

public class CustomMappingExample {
    public static void main(String[] args) {
        EmployeeMapper mapper = EmployeeMapper.INSTANCE;

        EmployeeDTO employeeDTO = new EmployeeDTO();
        employeeDTO.setEmpName("Rohit");
        employeeDTO.setEmpId("E001");

        Employee employee = mapper.toEmployee(employeeDTO);
        System.out.println("Employee: " + employee.getName() + ", ID: " + employee.getId());

        EmployeeDTO mappedEmployeeDTO = mapper.toEmployeeDTO(employee);
        System.out.println("EmployeeDTO: " + mappedEmployeeDTO.getEmpName() + ", ID: " + mappedEmployeeDTO.getEmpId());
    }
}

Explanation: This example demonstrates how to define custom mappings between EmployeeDTO and Employee using the @Mapping annotation.

Output

Employee: Rohit, ID: E001
EmployeeDTO: Rohit, ID: E001

Nested Mappings

MapStruct supports nested mappings, allowing you to map nested objects.

Defining the Beans

public class AddressDTO {
    private String street;
    private String city;

    // Getters and Setters
}

public class EmployeeDTO {
    private String empName;
    private String empId;
    private AddressDTO address;

    // Getters and Setters
}
public class Address {
    private String streetName;
    private String cityName;

    // Getters and Setters
}

public class Employee {
    private String name;
    private String id;
    private Address address;

    // Getters and Setters
}

Creating the Mapper

@Mapper
public interface EmployeeMapper {
    EmployeeMapper INSTANCE = Mappers.getMapper(EmployeeMapper.class);

    @Mapping(source = "empName", target = "name")
    @Mapping(source = "empId", target = "id")
    @Mapping(source = "address.street", target = "address.streetName")
    @Mapping(source = "address.city", target = "address.cityName")
    Employee toEmployee(EmployeeDTO employeeDTO);

    @Mapping(source = "name", target = "empName")
    @Mapping(source = "id", target = "empId")
    @Mapping(source = "address.streetName", target = "address.street")
    @Mapping(source = "address.cityName", target = "address.city")
    EmployeeDTO toEmployeeDTO(Employee employee);
}

Using the Mapper

public class NestedMappingExample {
    public static void main(String[] args) {
        EmployeeMapper mapper = EmployeeMapper.INSTANCE;

        AddressDTO addressDTO = new AddressDTO();
        addressDTO.setStreet("MG Road");
        addressDTO.setCity("Bangalore");

        EmployeeDTO employeeDTO = new EmployeeDTO();
        employeeDTO.setEmpName("Vikas");
        employeeDTO.setEmpId("E123");
        employeeDTO.setAddress(addressDTO);

        Employee employee = mapper.toEmployee(employeeDTO);

        System.out.println("Employee: " + employee.getName() + ", ID: " + employee.getId() +
                ", Address: " + employee.getAddress().getStreetName() + ", " + employee.getAddress().getCityName());

        EmployeeDTO mappedEmployeeDTO = mapper.toEmployeeDTO(employee);
        System.out.println("EmployeeDTO: " + mappedEmployeeDTO.getEmpName() + ", ID: " + mappedEmployeeDTO.getEmpId() +
                ", Address: " + mappedEmployeeDTO.getAddress().getStreet() + ", " + mappedEmployeeDTO.getAddress().getCity());
    }
}

Explanation: This example demonstrates how to map nested objects between EmployeeDTO and Employee using MapStruct.

Output

Employee: Vikas, ID: E123, Address: MG Road, Bangalore
EmployeeDTO: Vikas, ID: E123, Address: MG Road, Bangalore

Custom Type Conversions

You can define custom-type conversions using methods in the mapper interface.

Defining the Beans

public class ProductDTO {
    private String name;
    private String price;

    // Getters and Setters
}
public class Product {
    private String name;
    private double price;

    // Getters and Setters
}

Creating the Mapper

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;

@Mapper
public interface ProductMapper {
    ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class);

    @Mapping(source = "price", target = "price", qualifiedByName = "priceToDouble")
    Product toProduct(ProductDTO productDTO);

    @Mapping(source = "price", target = "price", qualifiedByName = "doubleToPrice")
    ProductDTO toProductDTO(Product product);

    @Named("priceToDouble")
    static double priceToDouble(String price) {
        return Double.parseDouble(price.replace("₹", "").trim());
    }

    @Named("doubleToPrice")
    static String doubleToPrice(double price) {
        return "₹ " + String.format("%.2f", price);
    }
}

Using the Mapper

public class CustomConversionExample {
    public static void main(String[] args) {
        ProductMapper mapper = ProductMapper.INSTANCE;

        ProductDTO productDTO = new ProductDTO();
        productDTO.setName("Laptop");
        productDTO.setPrice("₹ 50000");

        Product product = mapper.toProduct(productDTO);

        System.out.println("Product: " + product.getName() + ", Price: " + product.getPrice());

        ProductDTO mappedProductDTO = mapper.toProductDTO(product);
        System.out.println("ProductDTO: " + mappedProductDTO.getName() + ", Price: " + mappedProductDTO.getPrice());
    }
}

Explanation: This example demonstrates how to define custom-type conversions using the @Named annotation for methods within the mapper interface.

Output

Product: Laptop, Price: 50000.0
ProductDTO: Laptop, Price: ₹ 50000.00

Using MapStruct in Spring Boot

MapStruct can be seamlessly integrated with Spring Boot applications. Here's how you can use MapStruct in a Spring Boot project.

Adding MapStruct and Spring Boot Dependencies

Add the following dependencies to your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.2.Final</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

Defining the Beans

public class UserDTO {
    private String firstName;
    private String lastName;
    private String email;

    // Getters and Setters
}

public class User {
    private String givenName;
    private String familyName;
    private String emailAddress;

    // Getters and Setters
}

Creating the Mapper

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import org.springframework.stereotype.Component;

@Mapper(componentModel = "spring")
@Component
public interface UserMapper {
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

    @Mapping(source = "firstName", target = "givenName")
    @Mapping(source = "lastName", target = "familyName")
    @Mapping(source = "email", target = "emailAddress")
    User toUser(UserDTO userDTO);

    @Mapping(source = "givenName", target = "firstName")
    @Mapping(source = "familyName", target = "lastName")
    @Mapping(source = "emailAddress", target = "email")
    UserDTO toUserDTO(User user);
}

Creating the Spring Boot Application

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MapStructSpringBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(MapStructSpringBootApplication.class, args);
    }
}

Creating a REST Controller

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {   

 @Autowired
    private UserMapper userMapper;

    @PostMapping
    public UserDTO createUser(@RequestBody UserDTO userDTO) {
        User user = userMapper.toUser(userDTO);
        // Simulate saving the user to the database
        return userMapper.toUserDTO(user);
    }
}

Using the REST API

You can test the REST API using a tool like Postman. Make a POST request to http://localhost:8080/users with the following JSON body:

{
    "firstName": "Amit",
    "lastName": "Sharma",
    "email": "[email protected]"
}

Output

The response will be:

{
    "firstName": "Amit",
    "lastName": "Sharma",
    "email": "[email protected]"
}

Explanation: This example demonstrates how to integrate MapStruct with a Spring Boot application. The UserMapper interface is annotated with @Mapper(componentModel = "spring") to enable Spring dependency injection. The UserController uses the UserMapper to map between UserDTO and User objects in the REST API.

Conclusion

MapStruct is a powerful and efficient library for mapping Java beans. This guide covered the basics of setting up MapStruct, performing simple and nested mappings, custom type conversions, and complex nested examples. Additionally, it showed how to integrate MapStruct with a Spring Boot application. 

By leveraging MapStruct, you can simplify and enhance your data transfer logic in Java applications. For more detailed information and advanced features, refer to the official MapStruct documentation.

Comments