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
-
MapStruct Reference
-
Spring Integration
-
Lombok Integration
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: mapstruct for comprehensive documentation.