Backend Development 24 min read

Master Object Mapping in Java with MapStruct: From Basics to Advanced Techniques

This article introduces MapStruct, a powerful Java annotation‑based object‑mapping library, compares it with BeanUtils, shows how to integrate it into a Spring Boot project, and provides step‑by‑step examples for basic, collection, nested, composite, and advanced mappings including dependency injection, constants, custom processing, and exception handling.

macrozheng
macrozheng
macrozheng
Master Object Mapping in Java with MapStruct: From Basics to Advanced Techniques

When developing projects, we often need to convert between PO, VO, and DTO objects. Simple conversions can be handled with BeanUtils, but complex mappings require many getter and setter methods. This article recommends MapStruct, a powerful automatic object‑mapping tool.

About BeanUtils

I often use Hutool's BeanUtil for object conversion, but it has several drawbacks:

Property mapping relies on reflection, resulting in low performance.

Attributes with different names or types cannot be converted automatically, requiring manual getter/setter methods.

Nested objects also need manual handling.

Collection conversion requires explicit loops to copy each element.

MapStruct addresses all these shortcomings.

MapStruct Overview

MapStruct is a Java annotation‑based object‑property mapping tool with over 4.5K stars on GitHub. By defining mapping rules in an interface, MapStruct generates implementation classes at compile time, avoiding reflection and delivering excellent performance for complex mappings.

IDEA Plugin Support

As a popular mapping tool, MapStruct provides a dedicated IDEA plugin that can be installed before use.

Project Integration

Integrating MapStruct into a Spring Boot project is straightforward—just add the following two dependencies (using version 1.4.2.Final):
<code>&lt;dependency&gt;
    &lt;!-- MapStruct dependencies --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
        &lt;artifactId&gt;mapstruct&lt;/artifactId&gt;
        &lt;version&gt;${mapstruct.version}&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
        &lt;artifactId&gt;mapstruct-processor&lt;/artifactId&gt;
        &lt;version&gt;${mapstruct.version}&lt;/version&gt;
        &lt;scope&gt;compile&lt;/scope&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;</code>

Basic Usage

After integrating MapStruct, let's explore its capabilities.

Basic Mapping

We start with a quick introduction to MapStruct's core features and implementation principle.

First, define the member PO class

Member

:

<code>/**
 * Shopping member
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
    private Long id;
    private String username;
    private String password;
    private String nickname;
    private Date birthday;
    private String phone;
    private String icon;
    private Integer gender;
}</code>

Then define the member DTO class

MemberDto

(note the different field names and types):

<code>/**
 * Shopping member DTO
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberDto {
    private Long id;
    private String username;
    private String password;
    private String nickname;
    // Different type
    private String birthday;
    // Different name
    private String phoneNumber;
    private String icon;
    private Integer gender;
}</code>

Create a mapper interface

MemberMapper

to map same‑name properties, different‑name properties, and different‑type properties:

<code>@Mapper
public interface MemberMapper {
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);

    @Mapping(source = "phone", target = "phoneNumber")
    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
    MemberDto toDto(Member member);
}</code>

Use the mapper in a controller to test the conversion:

<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "Basic Mapping")
    @GetMapping("/baseMapping")
    public CommonResult baseTest() {
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        MemberDto memberDto = MemberMapper.INSTANCE.toDto(memberList.get(0));
        return CommonResult.success(memberDto);
    }
}
</code>

Running the project and testing the endpoint in Swagger shows that all PO properties have been successfully converted to the DTO.

MapStruct generates the implementation class based on the

@Mapper

and

@Mapping

annotations. You can inspect the generated code in the

target

directory.

The generated mapper implementation eliminates the need for manual getter/setter code.

Collection Mapping

MapStruct also supports collection mapping, allowing a list of PO objects to be converted to a list of DTOs automatically.

Add a

toDtoList

method to

MemberMapper

:

<code>@Mapper
public interface MemberMapper {
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);

    @Mapping(source = "phone", target = "phoneNumber")
    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
    List<MemberDto> toDtoList(List<Member> list);
}
</code>

Test the collection mapping in the controller:

<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "Collection Mapping")
    @GetMapping("/collectionMapping")
    public CommonResult collectionMapping() {
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        List<MemberDto> memberDtoList = MemberMapper.INSTANCE.toDtoList(memberList);
        return CommonResult.success(memberDtoList);
    }
}
</code>

Swagger confirms that the PO list has been transformed into a DTO list.

Nested Object Mapping

MapStruct can also map nested objects.

Define an

Order

PO that contains a

Member

and a list of

Product

objects:

<code>/**
 * Order
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Order {
    private Long id;
    private String orderSn;
    private Date createTime;
    private String receiverAddress;
    private Member member;
    private List<Product> productList;
}
</code>

Create the corresponding

OrderDto

with nested DTOs:

<code>/**
 * Order DTO
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class OrderDto {
    private Long id;
    private String orderSn;
    private Date createTime;
    private String receiverAddress;
    // Nested DTOs
    private MemberDto memberDto;
    private List<ProductDto> productDtoList;
}
</code>

Define

OrderMapper

and reuse existing mappers for nested objects:

<code>@Mapper(uses = {MemberMapper.class, ProductMapper.class})
public interface OrderMapper {
    OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);

    @Mapping(source = "member", target = "memberDto")
    @Mapping(source = "productList", target = "productDtoList")
    OrderDto toDto(Order order);
}
</code>

Test nested mapping in the controller:

<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "Nested Mapping")
    @GetMapping("/subMapping")
    public CommonResult subMapping() {
        List<Order> orderList = getOrderList();
        OrderDto orderDto = OrderMapper.INSTANCE.toDto(orderList.get(0));
        return CommonResult.success(orderDto);
    }
}
</code>

Swagger shows that nested objects have been correctly mapped.

Composite Mapping

MapStruct can merge properties from multiple source objects into a single target.

Define a composite DTO

MemberOrderDto

that extends

MemberDto

and adds order fields:

<code>/**
 * Member‑order composite DTO
 * Created by macro on 2021/10/21.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberOrderDto extends MemberDto {
    private String orderSn;
    private String receiverAddress;
}
</code>

Add a method to

MemberMapper

that maps both

Member

and

Order

to

MemberOrderDto

using qualified source names:

<code>@Mapper
public interface MemberMapper {
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);

    @Mapping(source = "member.phone", target = "phoneNumber")
    @Mapping(source = "member.birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
    @Mapping(source = "member.id", target = "id")
    @Mapping(source = "order.orderSn", target = "orderSn")
    @Mapping(source = "order.receiverAddress", target = "receiverAddress")
    MemberOrderDto toMemberOrderDto(Member member, Order order);
}
</code>

Test the composite mapping:

<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "Composite Mapping")
    @GetMapping("/compositeMapping")
    public CommonResult compositeMapping() {
        List<Order> orderList = LocalJsonUtil.getListFromJson("json/orders.json", Order.class);
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        Member member = memberList.get(0);
        Order order = orderList.get(0);
        MemberOrderDto dto = MemberMapper.INSTANCE.toMemberOrderDto(member, order);
        return CommonResult.success(dto);
    }
}
</code>

Swagger confirms that fields from both

Member

and

Order

are present in the composite DTO.

Advanced Usage

Having mastered the basics, we now explore some advanced MapStruct features.

Dependency Injection

Instead of using the static INSTANCE , we can let Spring inject the mapper by setting componentModel = "spring" on the @Mapper annotation.
<code>@Mapper(componentModel = "spring")
public interface MemberSpringMapper {
    @Mapping(source = "phone", target = "phoneNumber")
    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
    MemberDto toDto(Member member);
}
</code>
<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {

    @Autowired
    private MemberSpringMapper memberSpringMapper;

    @ApiOperation(value = "DI Mapping")
    @GetMapping("/springMapping")
    public CommonResult springMapping() {
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        MemberDto dto = memberSpringMapper.toDto(memberList.get(0));
        return CommonResult.success(dto);
    }
}
</code>

Constants, Default Values, and Expressions

MapStruct allows setting constant values, default values, or Java expressions for target properties.

Define

Product

and

ProductDto

where

id

is a constant,

count

has a default of 1, and

productSn

is generated via UUID.

<code>@Mapper(imports = {UUID.class})
public interface ProductMapper {
    ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class);

    @Mapping(target = "id", constant = "-1L")
    @Mapping(source = "count", target = "count", defaultValue = "1")
    @Mapping(target = "productSn", expression = "java(UUID.randomUUID().toString())")
    ProductDto toDto(Product product);
}
</code>
<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "Constants &amp; Defaults")
    @GetMapping("/defaultMapping")
    public CommonResult defaultMapping() {
        List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
        Product product = productList.get(0);
        product.setId(100L);
        product.setCount(null);
        ProductDto dto = ProductMapper.INSTANCE.toDto(product);
        return CommonResult.success(dto);
    }
}
</code>

Custom Processing Before and After Mapping

MapStruct supports @BeforeMapping and @AfterMapping methods similar to AOP.
<code>@Mapper(imports = {UUID.class})
public abstract class ProductRoundMapper {
    public static final ProductRoundMapper INSTANCE = Mappers.getMapper(ProductRoundMapper.class);

    @Mapping(target = "id", constant = "-1L")
    @Mapping(source = "count", target = "count", defaultValue = "1")
    @Mapping(target = "productSn", expression = "java(UUID.randomUUID().toString())")
    public abstract ProductDto toDto(Product product);

    @BeforeMapping
    public void beforeMapping(Product product) {
        // If price < 0, set to 0
        if (product.getPrice().compareTo(BigDecimal.ZERO) < 0) {
            product.setPrice(BigDecimal.ZERO);
        }
    }

    @AfterMapping
    public void afterMapping(@MappingTarget ProductDto productDto) {
        // Set current time as createTime
        productDto.setCreateTime(new Date());
    }
}
</code>
<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "Custom Pre/Post Processing")
    @GetMapping("/customRoundMapping")
    public CommonResult customRoundMapping() {
        List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
        Product product = productList.get(0);
        product.setPrice(new BigDecimal(-1));
        ProductDto dto = ProductRoundMapper.INSTANCE.toDto(product);
        return CommonResult.success(dto);
    }
}
</code>

Handling Mapping Exceptions

MapStruct can propagate exceptions thrown during mapping.

Create a custom exception

ProductValidatorException

and a validator that throws it when price is negative.

<code>public class ProductValidatorException extends Exception {
    public ProductValidatorException(String message) {
        super(message);
    }
}
</code>
<code>public class ProductValidator {
    public BigDecimal validatePrice(BigDecimal price) throws ProductValidatorException {
        if (price.compareTo(BigDecimal.ZERO) < 0) {
            throw new ProductValidatorException("Price cannot be less than 0!");
        }
        return price;
    }
}
</code>

Use the validator in a mapper and declare the exception:

<code>@Mapper(uses = {ProductValidator.class}, imports = {UUID.class})
public interface ProductExceptionMapper {
    ProductExceptionMapper INSTANCE = Mappers.getMapper(ProductExceptionMapper.class);

    @Mapping(target = "id", constant = "-1L")
    @Mapping(source = "count", target = "count", defaultValue = "1")
    @Mapping(target = "productSn", expression = "java(UUID.randomUUID().toString())")
    ProductDto toDto(Product product) throws ProductValidatorException;
}
</code>
<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "Exception Handling")
    @GetMapping("/exceptionMapping")
    public CommonResult exceptionMapping() {
        List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
        Product product = productList.get(0);
        product.setPrice(new BigDecimal(-1));
        ProductDto dto = null;
        try {
            dto = ProductExceptionMapper.INSTANCE.toDto(product);
        } catch (ProductValidatorException e) {
            e.printStackTrace();
        }
        return CommonResult.success(dto);
    }
}
</code>

The log shows the custom validation exception when the price is negative.

Conclusion

MapStruct is far more powerful than BeanUtils. For complex object mappings, it eliminates the need to write repetitive getter and setter code. The features demonstrated here are only a subset of its capabilities; interested readers should explore the official documentation for more advanced usage.

Reference

Official documentation: https://mapstruct.org/documentation/stable/reference/html

Source Code

GitHub repository: https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-mapstruct

JavaDTOSpring BootMapStructObject Mapping
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

login 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.