Mastering MapStruct

MapStruct

@Mapping, @AfterMapping, and @MappingTarget in Action

🧩 Introduction

In modern Java applications, especially with frameworks like Spring Boot, object mapping is a common task. Whether you’re transforming DTOs into entities or mapping between domain layers, doing this manually is tedious and error-prone.

MapStruct is a compile-time code generator that simplifies this task—fast, type-safe, and easy to use. In this article, we’ll explore three powerful annotations that give you more control and flexibility:

  • @Mapping
  • @AfterMapping
  • @MappingTarget

🔧 Setting the Stage: Basic Setup

Let’s assume we have a simple Spring Boot project with MapStruct configured.

Dependencies (Maven):

<properties>
 <java.version>21</java.version>
 <mapstruct.version>1.5.5.Final</mapstruct.version>
 <mvn.compile.version>3.11.0</mvn.compile.version>
</properties>

<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct</artifactId>
  <version>${mapstruct.version}</version>
</dependency>
<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct-processor</artifactId>
  <version>${mapstruct.version}</version>
  <scope>provided</scope>
</dependency>

<!-- and in build tag plugins -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>${mvn.compile.version}</version>
  <configuration>
    <annotationProcessorPaths>
      <path>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>${mapstruct.version}</version>
      </path>
      <!-- Other processors like Lombok can go here -->
    </annotationProcessorPaths>
  </configuration>
</plugin>

Make sure annotation processing is enabled in your IDE or build tool.


✅ Example Scenario: Mapping User DTO to Entity

Let’s say you have:

UserDTO.java

public record UserDTO(String firstName, String lastName, String email) {}
User.java
public class User {
    private String fullName;
    private String email;
    private LocalDateTime createdAt;
    // getters & setters
}

🎯 Using @Mapping to Customize Field Mapping

By default, MapStruct matches fields by name. But when the names don’t match (like firstName + lastNamefullName), you can use @Mapping.

UserMapper.java

@Mapper
public interface UserMapper {

    @Mapping(target = "fullName", expression = "java(dto.firstName() + \" \" + dto.lastName())")
    @Mapping(target = "createdAt", ignore = true) // we’ll handle this later
    User toEntity(UserDTO dto);
}

Here:

  • expression = "java(...)” lets you use Java code directly for custom logic.
  • ignore = true tells MapStruct to skip mapping that field.

🧪 Enhancing Mapping with @AfterMapping

Let’s say we want to set createdAt to the current time. This is where @AfterMapping comes in handy.

@Mapper
public interface UserMapper {

    @Mapping(target = "fullName", expression = "java(dto.firstName() + \" \" + dto.lastName())")
    @Mapping(target = "createdAt", ignore = true)
    User toEntity(UserDTO dto);

    @AfterMapping
    default void setCreatedAt(@MappingTarget User user) {
        user.setCreatedAt(LocalDateTime.now());
    }
}
  • @AfterMapping lets you customize the result after the auto-mapping is done.
  • @MappingTarget gives you access to the mapped target object.

🔁 Reusing Objects with @MappingTarget

Suppose you’re updating an existing User entity instead of creating a new one:

@Mapper
public interface UserMapper {

    @Mapping(target = "fullName", expression = "java(dto.firstName() + \" \" + dto.lastName())")
    @Mapping(target = "createdAt", ignore = true)
    void updateEntity(UserDTO dto, @MappingTarget User user);
}
  • This avoids creating a new object.
  • Great for PATCH/update endpoints where you fetch an entity from the DB, then update some fields.

🧠 Wrap-Up

These three annotations are the foundation of writing powerful, flexible, and clean mappers with MapStruct:

AnnotationPurpose
@MappingCustomize individual field mapping
@AfterMappingAdd post-processing logic
@MappingTargetModify an existing object instead of creating a new one

Mastering these tools helps you avoid boilerplate, improve readability, and maintain a clean domain model.


📦 Bonus: When Should You Use These?

  • Use @Mapping when field names/types differ or when you want custom logic.
  • Use @AfterMapping for audit fields (createdAt, updatedBy) or conditional logic.
  • Use @MappingTarget when updating existing entities (e.g., in services).