mapstruct

v2026.09.24

MapStruct Java mapping framework. Covers basic mappers, update mappings, nested objects, collections, custom methods, and Lombok integration. Based on castellino and gestionale-presenze production patterns. USE WHEN: user mentions "mapstruct", "@Mapper", "@Mapping", asks about "DTO mapping", "entity conversion", "@MappingTarget", "nested mapping" DO NOT USE FOR: Java language - use `java` skill instead DO NOT USE FOR: Lombok - use `lombok` skill instead DO NOT USE FOR: JPA entities - use JPA-specific skills

GitHub
Install command
npx skhub add claude-dev-suite/mapstruct
Markdown
SKILL.md

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

AnnotationPurpose
@MapperDefine mapper interface/abstract class
@MappingField-level mapping configuration
@MappingTargetUpdate existing object
@BeanMappingBean-level mapping settings
@AfterMappingPost-processing method
@BeforeMappingPre-processing method
@ConditionConditional mapping
@ValueMappingEnum value mapping

Best Practices

  1. Use componentModel = "spring" for Spring injection
  2. Use unmappedTargetPolicy = ReportingPolicy.IGNORE for DTOs with fewer fields
  3. Always exclude id, createdAt, updatedAt when mapping from DTOs
  4. Use @MappingTarget for partial updates
  5. Order annotation processors: Lombok → MapStruct → Binding
  6. Use abstract classes instead of interfaces for custom logic
  7. Use @Condition for conditional mapping logic

When NOT to Use This Skill

ScenarioUse Instead
Java language featuresjava skill
Lombok annotationslombok skill
Spring configurationbackend-spring-boot skill
JPA entity operationsJPA-specific skills
Simple copyingManual mapping or BeanUtils

Anti-Patterns

Anti-PatternWhy It's BadCorrect Approach
Mapping entities to entitiesBreaks change trackingMap DTO to entity
Not using @MappingTargetInefficient updatesUse for partial updates
Complex logic in expressionsHard to testUse custom methods
Ignoring all unmappedMisses fields silentlyUse WARN or ERROR policy
Not excluding audit fieldsOverwrites metadataExclude id, timestamps
Circular referencesStackOverflowErrorBreak cycles with custom mapping
Using interfaces for custom logicCan't inject dependenciesUse abstract classes
Not testing mappersRuntime mapping errorsWrite mapper tests

Quick Troubleshooting

IssueCauseSolution
"Cannot find implementation"Annotation processing failedCheck processor configuration
Lombok fields not foundWrong processor orderLombok before MapStruct
Mapper not autowiredWrong componentModelUse componentModel = "spring"
Circular dependencyMappers reference each otherUse @Lazy or refactor
UnmappedTargetProperty warningMissing mappingAdd @Mapping or ignore policy
NullPointerException in mappingNull sourceAdd null checks or NullValuePropertyMappingStrategy
Custom method not calledWrong signatureMatch method parameters exactly
Generated code not updatedIDE cacheClean and rebuild project

Reference Documentation

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: mapstruct for comprehensive documentation.

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/languages/mapstruct

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1