Mastering MapStruct: Simplify Java DTO Mapping with Real-World Examples

Learn how to efficiently convert between Java entities and DTOs using MapStruct, covering basic mappings, default methods, abstract classes, multiple parameters, update operations, Spring integration, and custom type conversions, with complete code examples and explanations to streamline data transfer in backend applications.

Programmer DD
Programmer DD
Programmer DD
Mastering MapStruct: Simplify Java DTO Mapping with Real-World Examples

1. What is MapStruct?

MapStruct is a compile‑time code generator that creates type‑safe mappers for converting between Java objects, such as entities and DTOs, eliminating the boilerplate of manual getter/setter copying.

2. Example Scenario

Suppose a User entity contains fields id, username, password, phoneNum, email, and a nested Role object. In a controller we only need id, username, and the role name.

@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 {
    /** user id */
    private Long userId;
    /** username */
    private String name;
    /** role name */
    private String roleName;
}

Manually copying these fields with getters and setters becomes cumbersome, especially when the number of properties grows.

3. Using MapStruct to Solve the Problem

Create a mapper interface that 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);
}

Test the mapper:

public class MainTest {
    User user = null;

    @Before
    public void before() {
        Role role = new Role(2L, "administrator", "超级管理员");
        user = new User(1L, "zhangsan", "12345", "17677778888", "[email protected]", role);
    }

    @Test
    public void test1() {
        UserRoleDto dto = new UserRoleDto();
        dto.setUserId(user.getId());
        dto.setName(user.getUsername());
        dto.setRoleName(user.getRole().getRoleName());
        System.out.println(dto);
    }
}

The mapper automatically copies the required fields, greatly simplifying the code.

4. Adding Default Methods

Mapper interfaces can also contain default methods for custom logic:

@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;
    }
}

Unit test can invoke defaultConvert() to obtain a placeholder DTO.

5. Using an Abstract Class Instead of an Interface

MapStruct also supports abstract classes as mappers:

@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;
    }
}

6. Mapping Multiple Source Parameters

MapStruct can bind fields from several source objects:

@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);
}

7. Using Parameters Directly as Values

Parameters that are not objects can also be mapped:

@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);
}

8. Updating Existing Objects with @MappingTarget

Instead of creating a new DTO, MapStruct can update an existing instance:

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);
}

9. Mapping Without Getters/Setters

Even classes without accessor methods can be mapped by directly accessing fields:

@Mapper
public interface CustomerMapper {
    CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);

    @Mapping(source = "customerName", target = "name")
    Customer toCustomer(CustomerDto dto);

    @InheritInverseConfiguration
    CustomerDto fromCustomer(Customer customer);
}

10. Spring Integration

Setting componentModel = "spring" registers the mapper as a Spring bean:

@Mapper(componentModel = "spring")
public interface CustomerMapper {
    @Mapping(source = "name", target = "customerName")
    CustomerDto toCustomerDto(Customer customer);
}

It can then be injected with @Autowired in any Spring component.

11. Custom Type Conversions

When source and target types differ, a helper class can be referenced via uses:

public class BooleanStrFormat {
    public String toStr(Boolean isDisable) {
        return isDisable ? "Y" : "N";
    }
    public Boolean toBoolean(String str) {
        return "Y".equals(str);
    }
}
@Mapper(uses = {BooleanStrFormat.class})
public interface CustomerMapper {
    CustomerMapper INSTANCES = Mappers.getMapper(CustomerMapper.class);

    @Mappings({
        @Mapping(source = "name", target = "customerName"),
        @Mapping(source = "isDisable", target = "disable"))
    })
    CustomerDto toCustomerDto(Customer customer);
}

When the mapper is used in a Spring context, the helper class should also be a Spring bean and will be injected automatically.

These examples demonstrate how MapStruct can dramatically reduce boilerplate code for object conversion, support default and custom methods, work with multiple sources, update existing instances, and integrate seamlessly with Spring.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavadtomapstructObject MappingSpring Integration
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.