Using MapStruct for Efficient Java Object Mapping and DTO Conversion
This article explains how MapStruct simplifies Java object‑to‑DTO mapping by defining mapping rules, demonstrates various features such as default methods, abstract mappers, multiple parameters, @MappingTarget updates, handling of fields without getters/setters, Spring integration, and custom type conversions with clear code examples.
MapStruct is a Java annotation processor that generates mapper implementations to copy data between objects, reducing boilerplate code when converting entities to DTOs.
1. What MapStruct Is Used For
When a User entity and a related Role entity need only a subset of fields (id, username, roleName) in a controller, a DTO (e.g., UserRoleDto ) can be created to carry just those fields.
@AllArgsConstructor
@Data
public class User {
private Long id;
private String username;
private String password;
private String phoneNum;
private String email;
private Role role;
}
@AllArgsConstructor
@Data
public class Role {
private Long id;
private String roleName;
private String description;
}
@Data
public class UserRoleDto {
private Long userId;
private String name;
private String roleName;
}Manually copying these fields with getters and setters is cumbersome, especially for many properties.
2. Solving the Problem with MapStruct
A mapper interface defines the mapping rules between User , Role , and UserRoleDto :
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UserRoleMapper {
UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);
@Mappings({
@Mapping(source = "id", target = "userId"),
@Mapping(source = "username", target = "name"),
@Mapping(source = "role.roleName", target = "roleName")
})
UserRoleDto toUserRoleDto(User user);
}Tests show the mapper creates the DTO with a single method call.
3. Adding Default Methods
Default methods can provide additional functionality inside the mapper interface:
@Mapper
public interface UserRoleMapper {
UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);
@Mappings({
@Mapping(source = "id", target = "userId"),
@Mapping(source = "username", target = "name"),
@Mapping(source = "role.roleName", target = "roleName")
})
UserRoleDto toUserRoleDto(User user);
default UserRoleDto defaultConvert() {
UserRoleDto dto = new UserRoleDto();
dto.setUserId(0L);
dto.setName("None");
dto.setRoleName("None");
return dto;
}
}4. Using an Abstract Class Instead of an Interface
The mapper can be an abstract class, allowing non‑abstract helper methods while still letting MapStruct generate the implementation:
@Mapper
public abstract class UserRoleMapper {
public static final UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);
@Mappings({
@Mapping(source = "id", target = "userId"),
@Mapping(source = "username", target = "name"),
@Mapping(source = "role.roleName", target = "roleName")
})
public abstract UserRoleDto toUserRoleDto(User user);
public UserRoleDto defaultConvert() {
UserRoleDto dto = new UserRoleDto();
dto.setUserId(0L);
dto.setName("None");
dto.setRoleName("None");
return dto;
}
}5. Mapping Multiple Source Parameters
MapStruct can map fields from two source objects into one target DTO:
@Mapper
public interface UserRoleMapper {
UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);
@Mappings({
@Mapping(source = "user.id", target = "userId"),
@Mapping(source = "user.username", target = "name"),
@Mapping(source = "role.roleName", target = "roleName")
})
UserRoleDto toUserRoleDto(User user, Role role);
}6. Using Method Parameters Directly as Values
Parameters that are not objects can be mapped directly:
@Mapper
public interface UserRoleMapper {
UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);
@Mappings({
@Mapping(source = "user.id", target = "userId"),
@Mapping(source = "user.username", target = "name"),
@Mapping(source = "myRoleName", target = "roleName")
})
UserRoleDto useParameter(User user, String myRoleName);
}7. Updating an Existing Object with @MappingTarget
Instead of creating a new instance, MapStruct can update an existing target object:
public interface UserRoleMapper1 {
UserRoleMapper1 INSTANCES = Mappers.getMapper(UserRoleMapper1.class);
@Mappings({
@Mapping(source = "userId", target = "id"),
@Mapping(source = "name", target = "username"),
@Mapping(source = "roleName", target = "role.roleName")
})
void updateDto(UserRoleDto dto, @MappingTarget User user);
@Mappings({
@Mapping(source = "id", target = "userId"),
@Mapping(source = "username", target = "name"),
@Mapping(source = "role.roleName", target = "roleName")
})
void update(User user, @MappingTarget UserRoleDto dto);
}8. Mapping Without Getters/Setters
Fields that are public or lack accessor methods can still be mapped:
@Mapper
public interface CustomerMapper {
CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);
@Mapping(source = "customerName", target = "name")
Customer toCustomer(CustomerDto dto);
@InheritInverseConfiguration
CustomerDto fromCustomer(Customer customer);
}9. Spring Dependency Injection
Setting componentModel = "spring" registers the mapper as a Spring bean, allowing it to be @Autowired:
@Mapper(componentModel = "spring")
public interface CustomerMapper {
@Mapping(source = "name", target = "customerName")
CustomerDto toCustomerDto(Customer customer);
}In a Spring Boot application the mapper can be injected and used in a REST controller.
10. Custom Type Conversions
When source and target types differ (e.g., Boolean to String ), a helper class can be referenced via uses :
public class BooleanStrFormat {
public String toStr(Boolean b) { return b ? "Y" : "N"; }
public Boolean toBoolean(String s) { return "Y".equals(s); }
}
@Mapper(uses = { BooleanStrFormat.class })
public interface CustomerMapper {
@Mappings({
@Mapping(source = "name", target = "customerName"),
@Mapping(source = "isDisable", target = "disable")
})
CustomerDto toCustomerDto(Customer customer);
}When the mapper is a Spring component, the helper class is also injected automatically.
Overall, MapStruct provides a concise, compile‑time safe way to map between Java objects, supporting default methods, abstract classes, multiple sources, target updates, Spring integration, and custom conversions.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.