Core Java

MapStruct: Map Source Object To Target List Example

Mapping between objects is a common requirement in Java applications, especially when transforming DTOs to Entities or vice versa. MapStruct simplifies this process by generating type-safe mappers at compile time. Let us delve into understanding how to map a source object to a target list using MapStruct.

1. Introduction

MapStruct is a Java annotation-based code generator that greatly simplifies the implementation of mappings between Java bean types. It allows developers to focus on business logic while MapStruct automatically generates the boilerplate code required for data transformation. This leads to cleaner, faster, and less error-prone code, especially in large-scale enterprise applications.

MapStruct is typically used to:

  • Map between DTOs and entities in service or controller layers
  • Convert complex, nested objects from one model to another
  • Eliminate repetitive and error-prone manual mapping logic
  • Enhance maintainability and readability of your application’s mapping layer

1.1 Example use cases

MapStruct can be applied in a variety of real-world development scenarios such as:

  • Mapping database entities to REST API response DTOs in Spring Boot applications
  • Transforming incoming form data into service-layer or domain-specific objects
  • Integrating with third-party services that require specific payload structures
  • Mapping between internal models and external contracts in microservices architectures

2. Code Example

This example demonstrates how to map a list of source objects (User) to a list of target DTOs (UserDTO) using MapStruct. We’ll walk through entity creation, mapping logic, decorators for post-processing, and qualifiers for reusable custom methods. By the end, you’ll have a clean, production-ready mapping setup.

Before diving into the example, make sure to update your project’s pom.xml with the following dependencies and plugin configuration.

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.5.5.Final</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.5.5.Final</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2.1 Entity Class

The User class represents the source entity that we want to map. It contains basic fields like firstName, lastName, and age. This is a typical Java POJO (Plain Old Java Object) with constructors, getters, and setters that make it compatible with MapStruct’s mapping engine.

package com.example.mapstruct.model;

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

    public User() {}
    public User(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    // Getters and setters
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }

    public void setFirstName(String firstName) { this.firstName = firstName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
    public void setAge(int age) { this.age = age; }
}

2.2 DTO Class

The UserDTO class is the target object that we want to map our source User entity into. It contains a derived fullName field and a boolean isAdult flag, both of which will be computed during the mapping process. Like the entity, this is also a POJO with standard getter/setter methods.

package com.example.mapstruct.model;

public class UserDTO {
    private String fullName;
    private boolean isAdult;

    // Getters and setters
    public String getFullName() { return fullName; }
    public boolean isAdult() { return isAdult; }

    public void setFullName(String fullName) { this.fullName = fullName; }
    public void setAdult(boolean isAdult) { this.isAdult = isAdult; }

    @Override
    public String toString() {
        return "UserDTO{fullName='" + fullName + "', isAdult=" + isAdult + "}";
    }
}

2.3 Qualifier Annotation

This is a custom qualifier annotation used to mark mapping methods that should be used in specific transformation scenarios. We’ll use it to identify methods that convert strings to title case, allowing MapStruct to select them using the @Qualifier annotation.

package com.example.mapstruct.mapper;

import org.mapstruct.Qualifier;
import java.lang.annotation.*;

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface TitleCaseMapper {}

2.4 Utility Class for Title Case

The NameUtils class provides reusable helper methods, including a method to convert any string into a title case. This method is annotated with our custom @TitleCaseMapper qualifier and will be used in the decorator to enhance the final output.

package com.example.mapstruct.mapper;

import java.util.Arrays;
import java.util.stream.Collectors;

public class NameUtils {

    @TitleCaseMapper
    public String toTitleCase(String name) {
        if (name == null || name.isEmpty()) return name;
        return Arrays.stream(name.split(" "))
                     .map(word ->vg word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase())
                     .collect(Collectors.joining(" "));
    }
}

2.5 Mapper Interface

The UserMapper interface is where we define the actual mapping rules using MapStruct annotations. We use the @Mapper annotation to make MapStruct generate the implementation, and @Mapping with expressions to compute derived fields such as fullName and isAdult. The interface is also decorated to allow post-processing.

package com.example.mapstruct.mapper;

import com.example.mapstruct.model.User;
import com.example.mapstruct.model.UserDTO;
import org.mapstruct.*;
import java.util.List;

@Mapper(componentModel = "default", uses = NameUtils.class)
@DecoratedWith(UserMapperDecorator.class)
public interface UserMapper {

    @Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
    @Mapping(target = "isAdult", expression = "java(user.getAge() >= 18)")
    UserDTO toUserDTO(User user);

    List<UserDTO> toUserDTOList(List<User> users);
}

2.6 Decorator Class

The UserMapperDecorator class is an abstract implementation of the UserMapper interface. It allows us to override and enhance the behavior of the generated mapper by applying custom logic (e.g., converting full name to title case). The decorator wraps around the generated mapper and provides a hook for post-mapping enhancements.

package com.example.mapstruct.mapper;

import com.example.mapstruct.model.User;
import com.example.mapstruct.model.UserDTO;

import java.util.List;
import java.util.stream.Collectors;

public abstract class UserMapperDecorator implements UserMapper {

    private UserMapper delegate;

    public void setDelegate(UserMapper delegate) {
        this.delegate = delegate;
    }

    @Override
    public UserDTO toUserDTO(User user) {
        UserDTO dto = delegate.toUserDTO(user);
        // Decorate: Convert full name to Title Case
        dto.setFullName(new NameUtils().toTitleCase(dto.getFullName()));
        return dto;
    }

    @Override
    public List<UserDTO> toUserDTOList(List<User> users) {
        return users.stream()
                    .map(this::toUserDTO)
                    .collect(Collectors.toList());
    }
}

2.7 Main Runner Class

This is the main driver class used to run the mapping logic. We create a list of User objects, pass them through the mapper, and print the resulting UserDTO list. This demonstrates how the full transformation pipeline works — from raw entities to decorated DTOs.

package com.example.mapstruct;

import com.example.mapstruct.mapper.UserMapper;
import com.example.mapstruct.model.User;
import com.example.mapstruct.model.UserDTO;
import org.mapstruct.factory.Mappers;

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        UserMapper mapper = Mappers.getMapper(UserMapper.class);

        List<User> users = Arrays.asList(
            new User("john", "doe", 25),
            new User("jane", "smith", 17)
        );

        List<UserDTO> dtos = mapper.toUserDTOList(users);
        dtos.forEach(System.out::println);
    }
}

2.8 Code Output

After running the Main class, the console will print the following output:

UserDTO{fullName='John Doe', isAdult=true}
UserDTO{fullName='Jane Smith', isAdult=false}

Let’s walk through how the above output is produced:

In the Main class, two User objects are created: one for “john doe” (25 years old) and another for “jane smith” (17 years old). These objects are passed to the UserMapper instance using mapper.toUserDTOList(users). MapStruct processes each object using the toUserDTO() method. The toUserDTO() method combines firstName and lastName to create a full name (e.g., john doe), and evaluates whether age >= 18 to set isAdult. The result is then passed through the UserMapperDecorator, which uses NameUtils.toTitleCase() to capitalize each word in the full name (e.g., john doeJohn Doe). Finally, the decorated UserDTO objects are returned and printed using System.out.println(), triggering the toString() method inside UserDTO.

2.9 Mapping Flattened Structures into Lists

Sometimes the source object contains “flattened” or discrete fields that logically belong together and should be grouped into structured objects in the target model. A common example is when fields like section1Name, section1Department, section2Name, etc., exist independently in the source, but in the target, they need to be combined into a List<SectionDTO>.

Let’s define such a scenario where the source object has multiple sections stored in separate variables, and we want to consolidate these into a structured list of SectionDTO in the target.

2.9.1 Source Class with Flattened Section Fields

public class Course {
    private String section1Name;
    private Integer section1Topics;
    private String section2Name;
    private Integer section2Topics;

    // Getters and Setters
}

2.9.2 Target DTO with a List of SectionDTOs

public class CourseDTO {
    private List<SectionDTO> sections;

    // Getters and Setters
}

public class SectionDTO {
    private String name;
    private Integer topics;

    // Getters and Setters
}

2.9.3 Mapper Interface Handling Flattened to Structured Mapping

To map from a flattened structure to a list, we can use an expression inside @Mapping or delegate the logic to a method.

@Mapper(componentModel = "default")
public interface CourseMapper {

    @Mapping(target = "sections", expression = "java(mapSections(course))")
    CourseDTO toCourseDTO(Course course);

    default List<SectionDTO> mapSections(Course course) {
        List<SectionDTO> sections = new ArrayList<>();
        if (course.getSection1Name() != null || course.getSection1Topics() != null) {
            sections.add(new SectionDTO(course.getSection1Name(), course.getSection1Topics()));
        }
        if (course.getSection2Name() != null || course.getSection2Topics() != null) {
            sections.add(new SectionDTO(course.getSection2Name(), course.getSection2Topics()));
        }
        return sections;
    }
}

This demonstrates how MapStruct can be extended using Java methods when the default field-to-field mapping isn’t sufficient — such as transforming flattened data into structured objects within a collection.

This pattern helps when migrating legacy flat data models to modern, object-oriented designs, enables dynamic collection creation based on conditionally available data, and reduces clutter in DTOs while improving the reusability of composed objects like SectionDTO.

3. Conclusion

MapStruct is a powerful and efficient mapping tool for Java applications. Whether you’re working with basic or complex mappings, it simplifies code and increases maintainability. In this article, we explored mapping using custom expressions for dynamic fields, decorators to override default logic, and qualifiers to route mapping to specific methods. By mastering these techniques, you can create clean, modular, and powerful mapping layers in your Java applications.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

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

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button