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 doe
→ John 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.