Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 29

Streamlining Entity-DTO Conversion with ModelMapper

Recommended resources

Hello! Let's continue our journey into building robust backend APIs.

In our last lesson, you learned how to manually create Data Transfer Objects (DTOs) and write mapper classes to decouple your API from your internal domain entities. While this is a crucial skill for understanding the underlying pattern, you likely noticed that writing the conversion logic (new DTO(...), dto.setField(...), etc.) can become repetitive and error-prone, especially in large applications.

Today, we'll address this by learning how to automate entity-to-DTO conversion using a mapping library called ModelMapper. This will help you write cleaner, more maintainable code by eliminating boilerplate and focusing on your application's business logic.

1. From Manual to Automatic Mapping

In the previous lesson, we established a pattern where a Mapper component is responsible for converting between Entities and DTOs.

DTO and Entity Mapper Concept
This diagram shows the bidirectional flow we want to achieve: converting an Entity to a DTO for responses, and a DTO to an Entity for requests. Manually implementing this mapper can be tedious.

Libraries like ModelMapper automate this process. They use reflection and naming conventions to intelligently map data from a source object to a target object, often with just a single line of code.

ModelMapper: Source to Target Class Mapping
ModelMapper acts as a smart engine that takes a source object and a target class, and automatically handles the property-by-property mapping.

The core idea is simple: if the source object has a field named email and the destination object also has a field named email of a compatible type, ModelMapper will handle the transfer automatically.


2. Setting Up ModelMapper in a Spring Boot Project

To use ModelMapper within our Spring ecosystem, we need to do two things:

  1. Add it as a project dependency.
  2. Configure it as a Spring @Bean so that Spring's IoC container can manage it and we can inject it into our services.

Let's start with a quick reading that covers these essential setup steps.

Entity To DTO Conversion for a Spring REST API

This article from Baeldung, "Entity To DTO Conversion for a Spring REST API", clearly shows how to integrate ModelMapper. It will walk you through adding the Maven dependency and defining the ModelMapper bean.

Please read section 2, "Model Mapper". It's short and contains the two key code snippets for getting started: the pom.xml dependency and the @Bean configuration method. We will discuss the why of the @Bean configuration next.

By defining the ModelMapper object inside a method annotated with @Bean (typically in a @Configuration class), you are telling Spring: "Create an instance of ModelMapper for me, and keep it in your application context. When I ask for it later using @Autowired, give me this instance." This is the foundation of dependency injection for third-party libraries.


3. Hands-On: Automating Conversion with ModelMapper

Now for the practical part. We'll watch a comprehensive video tutorial that builds a simple REST API and uses ModelMapper to handle all the entity-DTO conversions. This will show you exactly how much boilerplate code is saved.

Spring Boot DTO ↔ Entity Conversion | ModelMapper Made Easy

The video "Spring Boot DTO ↔ Entity Conversion | ModelMapper Made Easy" by Shristi Tech Academy provides a fantastic step-by-step guide. We'll focus on the core mapping logic.

Please watch the video from 00:21:17 to 00:31:17. This covers the most important scenarios: Configuring the Bean (00:21:17 - 00:22:46): See how the @Bean is created in a dedicated @Configuration class. This is a best practice to keep configuration separate. DTO to Entity (00:22:46 - 00:25:58): In the addBook method, pay close attention to how mapper.map(bookRequest, Book.class) replaces several lines of manual setter calls. Entity to DTO (00:25:58 - 00:28:04): In the getById method, you'll see the reverse conversion: mapper.map(bookEntity, BookResponse.class). Handling Lists (00:28:04 - 00:31:17): This is a very common use case. Observe how Java's Stream API is combined with ModelMapper to convert a List<Book> to a List<BookResponse>.

As you saw, the core of ModelMapper is the map() method:

  • To convert an incoming DTO to an entity: modelMapper.map(dtoObject, Entity.class)
  • To convert an entity to a response DTO: modelMapper.map(entityObject, Dto.class)
  • To convert a list, you stream the source list and apply the map() operation to each element:
    List<Dto> dtoList = entityList.stream()
                                  .map(entity -> modelMapper.map(entity, Dto.class))
                                  .collect(Collectors.toList());
    

This elegant approach keeps your service layer clean and focused on business rules, not on the tedious mechanics of data transfer.

Test your understanding!

Recall the Product entity and ProductResponseDTO from our previous lesson.

Product Entity:

@Entity
public class Product {
    @Id @GeneratedValue private Long id;
    private String name;
    private String description;
    private BigDecimal price;
    private String internalSku;
    private int stockQuantity;
    // getters and setters...
}

ProductResponseDTO Record:

public record ProductResponseDTO(
    Long id,
    String name,
    String description,
    BigDecimal price
) {}

Assume you have a ProductService with an injected ModelMapper instance:

@Service
public class ProductService {

    @Autowired
    private ModelMapper modelMapper;

    @Autowired
    private ProductRepository productRepository;

    // Your task is to implement this method
    public List<ProductResponseDTO> getAllProducts() {
        // ...
    }
}

Implement the getAllProducts() method using ModelMapper and the Stream API to fetch all products from the repository and convert them to a list of ProductResponseDTO.

Show answer
public List<ProductResponseDTO> getAllProducts() {
    // 1. Fetch all entity objects from the database
    List<Product> products = productRepository.findAll();

    // 2. Stream the list, map each entity to a DTO, and collect the results
    return products.stream()
            .map(product -> modelMapper.map(product, ProductResponseDTO.class))
            .collect(Collectors.toList());
}

This single, fluent statement replaces the need for a for loop and manual object creation, showcasing the power of combining ModelMapper with modern Java features.


4. Handling Edge Cases: Matching Strategies

ModelMapper works perfectly when field names match exactly. But what happens when they don't, or when you have nested objects? For example, what if your DTO has a field locationName but your entity has a nested object location with a name property?

ModelMapper's default MatchingStrategy is STRICT. It requires that all destination properties have a matching and accessible source property. This can sometimes be too rigid.

Let's watch a short video that demonstrates a common problem with nested objects and how to solve it by changing the matching strategy.

Spring Boot DTO Tutorial - Entity to DTO Conversion using ModelMapper Library | In 4 Simple Steps

This video from the popular Java Guides channel demonstrates a frequent issue where ModelMapper fails to map nested object properties and presents a simple, effective solution.

Watch from 06:43 to 08:50. The video shows an API response where nested location data is missing. Pay attention to how a single line of configuration, setMatchingStrategy(MatchingStrategies.LOOSE), solves the problem.

Setting the matching strategy to LOOSE allows ModelMapper to be more intelligent, enabling it to match flattened properties (like location.name in the entity to locationName in the DTO). You can set this globally on your ModelMapper bean:

@Bean
public ModelMapper modelMapper() {
    ModelMapper modelMapper = new ModelMapper();
    modelMapper.getConfiguration()
        .setMatchingStrategy(MatchingStrategies.LOOSE);
    return modelMapper;
}

For even more complex scenarios where strategies aren't enough, ModelMapper provides powerful APIs for defining custom property mappings, converters, and conditions. These are more advanced topics, but you can explore them in the Baeldung "Guide to Using ModelMapper" (ID: [LINK](https://www.baeldung.com/java-modelmapper)) when you encounter a challenging mapping requirement.


Conclusion

Congratulations! You have now learned how to significantly improve your productivity and code quality by automating the DTO conversion process. This is a technique used in countless professional Java projects.

Key Takeaways:

  • Boilerplate Reduction: Libraries like ModelMapper eliminate the need for manual, repetitive mapping code.
  • Spring Integration: You can manage ModelMapper as a Spring @Bean and inject it anywhere in your application.
  • The map() Method: The modelMapper.map(source, Destination.class) method is the core of the library.
  • Handling Collections: ModelMapper integrates seamlessly with the Java Stream API to convert lists of objects.
  • Matching Strategies: You can configure ModelMapper's MatchingStrategy (e.g., to LOOSE) to handle more complex object structures and mismatches.

What's Next?

So far in this module, we've designed our API endpoints, defined clear data contracts with DTOs, and automated the mapping between our internal models and these contracts. Our API is becoming clean and well-structured.

But how do other developers (or even our future selves) know how to use it? The next logical step is to create clear documentation. In the next lesson, we will explore how to document our APIs using Swagger/OpenAPI with SpringDoc, which will automatically generate interactive documentation based on our controllers and DTOs.