Guide to ModelMapper Library in Java

Introduction to ModelMapper

ModelMapper is a powerful Java library for object mapping that simplifies the process of converting objects from one type to another. It is particularly useful for mapping DTOs (Data Transfer Objects) to entities and vice versa, often in the context of transferring data between different layers of an application. ModelMapper aims to make mapping easy by providing a simple and flexible API.

Key points about ModelMapper

ModelMapper is a powerful Java library that simplifies object mapping. Here are key points about ModelMapper:

  1. Convention Over Configuration:

    • ModelMapper uses a convention-based approach to automatically map objects. By default, it matches properties based on their names and types, reducing the need for explicit configurations. This convention-over-configuration principle makes mapping straightforward and intuitive.
    • For example, if a PersonDTO has a property firstName and the Person class has a property givenName, ModelMapper will automatically map these properties if their names and types match or if configured explicitly.
  2. Type Maps:

    • ModelMapper uses TypeMap objects to define and store mappings between source and destination types. TypeMap can be customized to handle complex mappings, including nested properties and specific conversion logic.
    • You can create a TypeMap and add custom mappings using methods like addMappings, where you specify the mapping logic for each property.
  3. Converters and Providers:

    • ModelMapper allows you to define custom Converter objects to handle specific type conversions that are not handled by default. Converters provide fine-grained control over the mapping process, allowing for custom transformation logic.
    • Providers can be used to control object instantiation. They can determine how new instances of destination objects are created during the mapping process, providing further customization.
  4. Validation and Configuration:

    • ModelMapper offers validation to ensure mappings are correct and complete. The validate method can be used to verify that all source and destination properties are correctly mapped, catching potential issues early in the development process.
    • Global configuration settings allow you to customize the behavior of ModelMapper, such as enabling or disabling implicit mapping, setting strict mapping rules, and adjusting property matching strategies.
  5. Nested and Complex Mappings:

    • ModelMapper excels at handling nested and complex mappings. It can automatically map nested objects and their properties, provided the property names and types match. This is particularly useful when dealing with complex object graphs, such as mapping DTOs with nested properties to entities.
    • Additionally, ModelMapper supports mapping collections and arrays, making it easy to map lists of objects between source and destination types.

Installation

Adding ModelMapper to Your Project

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

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>2.4.4</version> <!-- or the latest version -->
</dependency>

For Gradle:

implementation 'org.modelmapper:modelmapper:2.4.4'

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 and Performing the Mapping

import org.modelmapper.ModelMapper;

public class ModelMapperExample {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();

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

        Person person = modelMapper.map(personDTO, Person.class);

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

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

Output:

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

Explanation: This example demonstrates basic mapping between PersonDTO and Person objects using ModelMapper. The map method is used to convert from one type to another.

Advanced Features

Custom Mappings

You can define custom mappings using the TypeMap class.

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 and Custom Mapping

import org.modelmapper.ModelMapper;
import org.modelmapper.TypeMap;

public class CustomMappingExample {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();

        TypeMap<EmployeeDTO, Employee> typeMap = modelMapper.createTypeMap(EmployeeDTO.class, Employee.class);
        typeMap.addMappings(mapper -> {
            mapper.map(EmployeeDTO::getEmpName, Employee::setName);
            mapper.map(EmployeeDTO::getEmpId, Employee::setId);
        });

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

        Employee employee = modelMapper.map(employeeDTO, Employee.class);

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

        EmployeeDTO mappedEmployeeDTO = modelMapper.map(employee, EmployeeDTO.class);
        System.out.println("EmployeeDTO: " + mappedEmployeeDTO.getEmpName() + ", ID: " + mappedEmployeeDTO.getEmpId());
    }
}

Output:

Employee: Vikas, ID: E123
EmployeeDTO: Vikas, ID: E123

Explanation: This example demonstrates custom mappings using the TypeMap class to specify the source and target properties explicitly.

Nested Mappings

ModelMapper supports nested mappings, which allow 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 and Performing the Nested Mapping

import org.modelmapper.ModelMapper;

public class NestedMappingExample {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();

        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 = modelMapper.map(employeeDTO, Employee.class);

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

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

Output:

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

Explanation: This example demonstrates nested mappings, where nested objects and their properties are mapped using ModelMapper.

Custom Type Conversions

You can define custom-type conversions using Converter objects.

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 and Custom Type Conversions

import org.modelmapper.Converter;
import org.modelmapper.ModelMapper;
import org.modelmapper.spi.MappingContext;

public class CustomConversionExample {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();

        Converter<String, Double> stringToDouble = new Converter<String, Double>() {
            @Override
            public Double convert(MappingContext<String, Double> context) {
                return Double.parseDouble(context.getSource().replace("₹", "").trim());
            }
        };

        Converter<Double, String> doubleToString = new Converter<Double, String>() {
            @Override
            public String convert(MappingContext<Double, String> context) {
                return "₹ " + String.format("%.2f", context.getSource());
            }
        };

        modelMapper.createTypeMap(ProductDTO.class, Product.class)
                .addMappings(mapper -> mapper.using(stringToDouble).map(ProductDTO::getPrice, Product::setPrice));

        modelMapper.createTypeMap(Product.class, ProductDTO.class)
                .addMappings(mapper -> mapper.using(doubleToString).map(Product::getPrice, ProductDTO::setPrice));

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

        Product product = modelMapper.map(productDTO, Product.class);

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

        ProductDTO mappedProductDTO = modelMapper.map(product, ProductDTO.class);
        System.out.println("ProductDTO: " + mappedProductDTO.getName() + ", Price: " + mappedProductDTO.getPrice());
    }
}

Output:

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

Explanation: This example demonstrates custom-type conversions using Converter objects to handle specific type conversions.

Complex and Nested Examples

Mapping Complex-Nested Objects

Defining the Beans

import java.util.List;

public class CompanyDTO {
    private String name;
    private CEO ceo;
    private List<EmployeeDTO> employees;

    // Getters and Setters
}

public class CEO {
    private String name;

    // Getters and Setters
}

public class Company {
    private String companyName;
    private CEO ceo;
    private List<Employee> employees;

    // Getters and Setters
}

Creating the Mapper

import org.modelmapper.ModelMapper;
import org.modelmapper.TypeMap;

public class ComplexNestedMappingExample {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();

        TypeMap<CompanyDTO,

 Company> typeMap = modelMapper.createTypeMap(CompanyDTO.class, Company.class);
        typeMap.addMappings(mapper -> {
            mapper.map(CompanyDTO::getName, Company::setCompanyName);
            mapper.map(CompanyDTO::getCeo, Company::setCeo);
            mapper.map(CompanyDTO::getEmployees, Company::setEmployees);
        });

        CEO ceo = new CEO();
        ceo.setName("Rajesh");

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

        EmployeeDTO employeeDTO1 = new EmployeeDTO();
        employeeDTO1.setEmpName("Vikas");
        employeeDTO1.setEmpId("E123");
        employeeDTO1.setAddress(addressDTO1);

        AddressDTO addressDTO2 = new AddressDTO();
        addressDTO2.setStreet("Brigade Road");
        addressDTO2.setCity("Bangalore");

        EmployeeDTO employeeDTO2 = new EmployeeDTO();
        employeeDTO2.setEmpName("Priya");
        employeeDTO2.setEmpId("E124");
        employeeDTO2.setAddress(addressDTO2);

        CompanyDTO companyDTO = new CompanyDTO();
        companyDTO.setName("Tech Solutions");
        companyDTO.setCeo(ceo);
        companyDTO.setEmployees(Arrays.asList(employeeDTO1, employeeDTO2));

        Company company = modelMapper.map(companyDTO, Company.class);

        System.out.println("Company: " + company.getCompanyName());
        System.out.println("CEO: " + company.getCeo().getName());
        for (Employee employee : company.getEmployees()) {
            System.out.println("Employee: " + employee.getName() + ", ID: " + employee.getId() +
                    ", Address: " + employee.getAddress().getStreetName() + ", " + employee.getAddress().getCityName());
        }

        CompanyDTO mappedCompanyDTO = modelMapper.map(company, CompanyDTO.class);
        System.out.println("CompanyDTO: " + mappedCompanyDTO.getName());
        System.out.println("CEO: " + mappedCompanyDTO.getCeo().getName());
        for (EmployeeDTO employeeDTO : mappedCompanyDTO.getEmployees()) {
            System.out.println("EmployeeDTO: " + employeeDTO.getEmpName() + ", ID: " + employeeDTO.getEmpId() +
                    ", Address: " + employeeDTO.getAddress().getStreet() + ", " + employeeDTO.getAddress().getCity());
        }
    }
}

Output:

Company: Tech Solutions
CEO: Rajesh
Employee: Vikas, ID: E123, Address: MG Road, Bangalore
Employee: Priya, ID: E124, Address: Brigade Road, Bangalore
CompanyDTO: Tech Solutions
CEO: Rajesh
EmployeeDTO: Vikas, ID: E123, Address: MG Road, Bangalore
EmployeeDTO: Priya, ID: E124, Address: Brigade Road, Bangalore

Explanation: This example demonstrates mapping complex nested objects, including handling collections of nested objects. The CompanyDTO contains a nested CEO object and a list of EmployeeDTO objects, which are mapped to their respective entities.

Using ModelMapper in Spring Boot

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

Adding ModelMapper 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.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>3.2.0</version>
</dependency>

Creating the Spring Boot Application

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.modelmapper.ModelMapper;

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

    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}

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 and Service

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private ModelMapper modelMapper;
    
    // constructor-based DI
    public UserService(ModelMapper modelMapper){
    	this.modelMapper = modelMapper;
    }

    public UserDTO convertToDto(User user) {
        return modelMapper.map(user, UserDTO.class);
    }

    public User convertToEntity(UserDTO userDTO) {
        return modelMapper.map(userDTO, User.class);
    }
}

Creating a REST Controller

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

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

    private UserService userService;
    
    // constructor-based DI
    public UserController(UserService userService){
    	this.userService = userService;
    }

    @PostMapping
    public UserDTO createUser(@RequestBody UserDTO userDTO) {
        User user = userService.convertToEntity(userDTO);
        // Simulate saving the user to the database
        return userService.convertToDto(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": "amit.sharma@example.com"
}

Output:

The response will be:

{
    "firstName": "Amit",
    "lastName": "Sharma",
    "email": "amit.sharma@example.com"
}

Explanation: This example demonstrates how to integrate ModelMapper with a Spring Boot application. The ModelMapper bean is defined in the Spring Boot application class and is used in the UserService to map between UserDTO and User objects. The UserController uses the UserService to handle the REST API requests.

Conclusion

ModelMapper is a powerful and flexible library for object mapping in Java. This guide covered the basics of setting up ModelMapper, performing simple and nested mappings, custom-type conversions, and complex nested examples. Additionally, it showed how to integrate ModelMapper with a Spring Boot application. By leveraging ModelMapper, you can simplify and enhance your data transfer logic in Java applications. For more detailed information and advanced features, refer to the official ModelMapper documentation.

Comments