MapStruct Object Mapping
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: mapstruct for comprehensive documentation.
Basic Mapper
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
UserResponse toResponse(User user);
List<UserResponse> toResponseList(List<User> users);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
@Mapping(target = "password", ignore = true)
User toEntity(CreateUserRequest dto);
}
Update Mapping
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
// Partial update - ignores null values
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntity(UpdateUserRequest dto, @MappingTarget User user);
// Full update - sets all fields including nulls
void updateEntityFull(UpdateUserRequest dto, @MappingTarget User user);
}
Field Mapping
@Mapper(componentModel = "spring")
public interface OrderMapper {
// Different field names
@Mapping(source = "customer.name", target = "customerName")
@Mapping(source = "customer.email", target = "customerEmail")
@Mapping(source = "items", target = "orderItems")
OrderResponse toResponse(Order order);
// Constant values
@Mapping(target = "status", constant = "PENDING")
@Mapping(target = "createdAt", expression = "java(java.time.LocalDateTime.now())")
Order toEntity(CreateOrderRequest dto);
// Expression
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
UserResponse toResponse(User user);
// Date formatting
@Mapping(source = "createdAt", target = "createdDate", dateFormat = "yyyy-MM-dd")
OrderResponse toResponse(Order order);
}
Nested Objects
@Mapper(componentModel = "spring", uses = {AddressMapper.class, ItemMapper.class})
public interface OrderMapper {
// Uses AddressMapper for address field
// Uses ItemMapper for items collection
OrderResponse toResponse(Order order);
}
@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressResponse toResponse(Address address);
}
@Mapper(componentModel = "spring")
public interface ItemMapper {
ItemResponse toResponse(Item item);
}
Enum Mapping
@Mapper(componentModel = "spring")
public interface StatusMapper {
@ValueMappings({
@ValueMapping(source = "ACTIVE", target = "ENABLED"),
@ValueMapping(source = "INACTIVE", target = "DISABLED"),
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "UNKNOWN")
})
ExternalStatus toExternalStatus(InternalStatus status);
}
Custom Methods
@Mapper(componentModel = "spring")
public abstract class UserMapper {
@Autowired
protected RoleRepository roleRepository;
public abstract UserResponse toResponse(User user);
@Mapping(target = "roles", source = "roleIds")
public abstract User toEntity(CreateUserRequest dto);
// Custom mapping method
protected Set<Role> mapRoles(Set<Long> roleIds) {
if (roleIds == null) return new HashSet<>();
return roleIds.stream()
.map(id -> roleRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Role", "id", id)))
.collect(Collectors.toSet());
}
}
Collection Mapping
@Mapper(componentModel = "spring")
public interface ProductMapper {
ProductResponse toResponse(Product product);
List<ProductResponse> toResponseList(List<Product> products);
Set<ProductResponse> toResponseSet(Set<Product> products);
// Page mapping
default Page<ProductResponse> toResponsePage(Page<Product> products) {
return products.map(this::toResponse);
}
}
After/Before Mapping
@Mapper(componentModel = "spring")
public abstract class UserMapper {
@AfterMapping
protected void afterMapping(@MappingTarget UserResponse response, User user) {
response.setDisplayName(user.getFirstName() + " " + user.getLastName().charAt(0) + ".");
}
@BeforeMapping
protected void beforeMapping(CreateUserRequest dto) {
if (dto.getEmail() != null) {
dto.setEmail(dto.getEmail().toLowerCase().trim());
}
}
}
Conditional Mapping
@Mapper(componentModel = "spring")
public abstract class UserMapper {
@Condition
public boolean isNotEmpty(String value) {
return value != null && !value.trim().isEmpty();
}
// Only maps non-empty strings
UserResponse toResponse(User user);
}
Maven Configuration
<properties>
<mapstruct.version>1.6.2</mapstruct.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<!-- Order matters: Lombok first -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.defaultComponentModel=spring</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
Key Annotations
| Annotation |
Purpose |
@Mapper |
Define mapper interface/abstract class |
@Mapping |
Field-level mapping configuration |
@MappingTarget |
Update existing object |
@BeanMapping |
Bean-level mapping settings |
@AfterMapping |
Post-processing method |
@BeforeMapping |
Pre-processing method |
@Condition |
Conditional mapping |
@ValueMapping |
Enum value mapping |
Best Practices
- Use
componentModel = "spring" for Spring injection
- Use
unmappedTargetPolicy = ReportingPolicy.IGNORE for DTOs with fewer fields
- Always exclude
id, createdAt, updatedAt when mapping from DTOs
- Use
@MappingTarget for partial updates
- Order annotation processors: Lombok → MapStruct → Binding
- Use abstract classes instead of interfaces for custom logic
- Use
@Condition for conditional mapping logic
When NOT to Use This Skill
| Scenario |
Use Instead |
| Java language features |
java skill |
| Lombok annotations |
lombok skill |
| Spring configuration |
backend-spring-boot skill |
| JPA entity operations |
JPA-specific skills |
| Simple copying |
Manual mapping or BeanUtils |
Anti-Patterns
| Anti-Pattern |
Why It's Bad |
Correct Approach |
| Mapping entities to entities |
Breaks change tracking |
Map DTO to entity |
| Not using @MappingTarget |
Inefficient updates |
Use for partial updates |
| Complex logic in expressions |
Hard to test |
Use custom methods |
| Ignoring all unmapped |
Misses fields silently |
Use WARN or ERROR policy |
| Not excluding audit fields |
Overwrites metadata |
Exclude id, timestamps |
| Circular references |
StackOverflowError |
Break cycles with custom mapping |
| Using interfaces for custom logic |
Can't inject dependencies |
Use abstract classes |
| Not testing mappers |
Runtime mapping errors |
Write mapper tests |
Quick Troubleshooting
| Issue |
Cause |
Solution |
| "Cannot find implementation" |
Annotation processing failed |
Check processor configuration |
| Lombok fields not found |
Wrong processor order |
Lombok before MapStruct |
| Mapper not autowired |
Wrong componentModel |
Use componentModel = "spring" |
| Circular dependency |
Mappers reference each other |
Use @Lazy or refactor |
| UnmappedTargetProperty warning |
Missing mapping |
Add @Mapping or ignore policy |
| NullPointerException in mapping |
Null source |
Add null checks or NullValuePropertyMappingStrategy |
| Custom method not called |
Wrong signature |
Match method parameters exactly |
| Generated code not updated |
IDE cache |
Clean and rebuild project |
Reference Documentation
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: mapstruct for comprehensive documentation.