DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Writing DTOs With Java8, Lombok, and Java14+
  • User-Friendly API Publishing and Testing With Retrofit
  • Hibernate Validator vs Regex vs Manual Validation: Which One Is Faster?
  • Functional Approach To String Manipulation in Java

Trending

  • Concourse CI/CD Pipeline: Webhook Triggers
  • Blue Skies Ahead: An AI Case Study on LLM Use for a Graph Theory Related Application
  • Cookies Revisited: A Networking Solution for Third-Party Cookies
  • AI's Dilemma: When to Retrain and When to Unlearn?
  1. DZone
  2. Coding
  3. Java
  4. Generate Object Mapping Using MapStruct

Generate Object Mapping Using MapStruct

Do you write a lot of mapping code to map between different object models? Learn about MapStruct, which simplifies this task by generating mapping code.

By 
Gunter Rotsaert user avatar
Gunter Rotsaert
DZone Core CORE ·
May. 21, 24 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
4.9K Views

Join the DZone community and get the full member experience.

Join For Free

Do you need to write a lot of mapping code in order to map between different object models? MapStruct simplifies this task by generating mapping code. In this blog, you will learn some basic features of MapStruct. Enjoy!

Introduction

In a multi-layered application, one often has to write boilerplate code in order to map different object models. This can be a tedious and an error-prone task. MapStruct simplifies this task by generating the mapping code for you. It generates code during compile time and aims to generate the code as if it was written by you.

This blog will only give you a basic overview of how MapStruct can aid you, but it will be sufficient to give you a good impression of which problem it can solve for you.

If you are using IntelliJ as an IDE, you can also install the MapStruct Support Plugin which will assist you in using MapStruct.

Sources used in this blog can be found on GitHub.

Prerequisites

Prerequisites for this blog are:

  • Basic Java knowledge, Java 21 is used in this blog
  • Basic Spring Boot knowledge

Basic Application

The application used in this blog is a basic Spring Boot project. By means of a Rest API, a customer can be created and retrieved. In order to keep the API specification and source code in line with each other, you will use the openapi-generator-maven-plugin. First, you write the OpenAPI specification and the plugin will generate the source code for you based on the specification. The OpenAPI specification consists out of two endpoints, one for creating a customer (POST) and one for retrieving the customer (GET). The customer consists of its name and some address data.

YAML
 
Customer:
  type: object
  properties:
    firstName:
      type: string
      description: First name of the customer
      minLength: 1
      maxLength: 20
    lastName:
      type: string
      description: Last name of the customer
      minLength: 1
      maxLength: 20
    street:
      type: string
      description: Street of the customer
      minLength: 1
      maxLength: 20
    number:
      type: string
      description: House number of the customer
      minLength: 1
      maxLength: 5
    postalCode:
      type: string
      description: Postal code of the customer
      minLength: 1
      maxLength: 5
    city:
      type: string
      description: City of the customer
      minLength: 1
      maxLength: 20


The CustomerController implements the generated Controller interface. The OpenAPI maven plugin makes use of its own model. In order to transfer the data to the CustomerService, DTOs are created. These are Java records. The CustomerDto is:

Java
 
public record CustomerDto(Long id, String firstName, String lastName, AddressDto address) {
}


The AddressDto is:

Java
 
public record AddressDto(String street, String houseNumber, String zipcode, String city) {
}


The domain itself is used within the Service and is a basic Java POJO. The Customer domain is:

Java
 
public class Customer {
    private Long customerId;
    private String firstName;
    private String lastName;
 
    private Address address;
 
    // Getters and setters left out for brevity
}


The Address domain is:

Java
 
public class Address {
 
    private String street;
    private int houseNumber;
    private String zipcode;
    private String city;
 
    // Getters and setters left out for brevity
}


In order to connect everything together, you will need to write mapper code for:

  • Mapping between the API model and the DTO
  • Mapping between the DTO and the domain

Mapping Between DTO and Domain

Add Dependency

In order to make use of MapStruct, it suffices to add the MapStruct Maven dependency and to add some configuration to the Maven Compiler plugin.

XML
 
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${org.mapstruct.version}</version>
</dependency>
...
<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
        ...
    </plugins>
</build>


Create Mapper

The CustomerDto, AddressDto and the Customer, Address domains do not differ very much from each other.

  • CustomerDto has an id while Customer has a customerId.
  • AddressDto has a houseNumber of the type String while Address has a houseNumber of the type integer.

In order to create a mapper for this using MapStruct, you create an interface CustomerMapper, annotate it with @Mapper, and specify the component model with the value spring. Doing this will ensure that the generated mapper is a singleton-scoped Spring bean that can be retrieved via @Autowired.

Because both models are quite similar to each other, MapStruct will be able to generate most of the code by itself. Because the customer id has a different name in both models, you need to help MapStruct a bit. Using the @Mapping annotation, you specify the source and target mapping. For the type conversion, you do not need to do anything, MapStruct can sort this out based on the implicit type conversions.

The corresponding mapper code is the following:

Java
 
@Mapper(componentModel = "spring")
public interface CustomerMapper {
 
    @Mapping(source = "customerId", target = "id")
    CustomerDto transformToCustomerDto(Customer customer);
 
    @Mapping(source = "id", target = "customerId")
    Customer transformToCustomer(CustomerDto customerDto);
 
}


Generate the code:

Shell
 
$ mvn clean compile


In the target/generated-sources/annotations directory, you can find the generated CustomerMapperImpl class.

Java
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-04-21T13:38:51+0200",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 21 (Eclipse Adoptium)"
)
@Component
public class CustomerMapperImpl implements CustomerMapper {
 
    @Override
    public CustomerDto transformToCustomerDto(Customer customer) {
        if ( customer == null ) {
            return null;
        }
 
        Long id = null;
        String firstName = null;
        String lastName = null;
        AddressDto address = null;
 
        id = customer.getCustomerId();
        firstName = customer.getFirstName();
        lastName = customer.getLastName();
        address = addressToAddressDto( customer.getAddress() );
 
        CustomerDto customerDto = new CustomerDto( id, firstName, lastName, address );
 
        return customerDto;
    }
 
    @Override
    public Customer transformToCustomer(CustomerDto customerDto) {
        if ( customerDto == null ) {
            return null;
        }
 
        Customer customer = new Customer();
 
        customer.setCustomerId( customerDto.id() );
        customer.setFirstName( customerDto.firstName() );
        customer.setLastName( customerDto.lastName() );
        customer.setAddress( addressDtoToAddress( customerDto.address() ) );
 
        return customer;
    }
 
    protected AddressDto addressToAddressDto(Address address) {
        if ( address == null ) {
            return null;
        }
 
        String street = null;
        String houseNumber = null;
        String zipcode = null;
        String city = null;
 
        street = address.getStreet();
        houseNumber = String.valueOf( address.getHouseNumber() );
        zipcode = address.getZipcode();
        city = address.getCity();
 
        AddressDto addressDto = new AddressDto( street, houseNumber, zipcode, city );
 
        return addressDto;
    }
 
    protected Address addressDtoToAddress(AddressDto addressDto) {
        if ( addressDto == null ) {
            return null;
        }
 
        Address address = new Address();
 
        address.setStreet( addressDto.street() );
        if ( addressDto.houseNumber() != null ) {
            address.setHouseNumber( Integer.parseInt( addressDto.houseNumber() ) );
        }
        address.setZipcode( addressDto.zipcode() );
        address.setCity( addressDto.city() );
 
        return address;
    }
}


As you can see, the code is very readable and it has taken into account the mapping of Customer and Address.

Create Service

The Service will create a domain Customer taken the CustomerDto as an input. The customerMapper is injected into the Service and is used for converting between the two models. The other way around, when a customer is retrieved, the mapper converts the domain Customer to a CustomerDto. In the Service, the customers are persisted in a basic list in order to keep things simple.

Java
 
@Service
public class CustomerService {
 
    private final CustomerMapper customerMapper;
 
    private final HashMap<Long, Customer> customers = new HashMap<>();
    private Long index = 0L;
 
    CustomerService(CustomerMapper customerMapper) {
        this.customerMapper = customerMapper;
    }
 
    public CustomerDto createCustomer(CustomerDto customerDto) {
        Customer customer = customerMapper.transformToCustomer(customerDto);
        customer.setCustomerId(index);
        customers.put(index, customer);
        index++;
        return customerMapper.transformToCustomerDto(customer);
    }
 
    public CustomerDto getCustomer(Long customerId) {
        if (customers.containsKey(customerId)) {
            return customerMapper.transformToCustomerDto(customers.get(customerId));
        } else {
            return null;
        }
    }
}


Test Mapper

The mapper can be easily tested by using the generated CustomerMapperImpl class and verify whether the mappings are executed successfully.

Java
 
class CustomerMapperTest {
 
    @Test
    void givenCustomer_whenMaps_thenCustomerDto() {
        CustomerMapperImpl customerMapper = new CustomerMapperImpl();
        Customer customer = new Customer();
        customer.setCustomerId(2L);
        customer.setFirstName("John");
        customer.setLastName("Doe");
        Address address = new Address();
        address.setStreet("street");
        address.setHouseNumber(42);
        address.setZipcode("zipcode");
        address.setCity("city");
        customer.setAddress(address);
 
        CustomerDto customerDto = customerMapper.transformToCustomerDto(customer);
        assertThat( customerDto ).isNotNull();
        assertThat(customerDto.id()).isEqualTo(customer.getCustomerId());
        assertThat(customerDto.firstName()).isEqualTo(customer.getFirstName());
        assertThat(customerDto.lastName()).isEqualTo(customer.getLastName());
 
        AddressDto addressDto = customerDto.address();
        assertThat(addressDto.street()).isEqualTo(address.getStreet());
        assertThat(addressDto.houseNumber()).isEqualTo(String.valueOf(address.getHouseNumber()));
        assertThat(addressDto.zipcode()).isEqualTo(address.getZipcode());
        assertThat(addressDto.city()).isEqualTo(address.getCity());
    }
 
    @Test
    void givenCustomerDto_whenMaps_thenCustomer() {
        CustomerMapperImpl customerMapper = new CustomerMapperImpl();
        AddressDto addressDto = new AddressDto("street", "42", "zipcode", "city");
        CustomerDto customerDto = new CustomerDto(2L, "John", "Doe", addressDto);
 
        Customer customer = customerMapper.transformToCustomer(customerDto);
        assertThat( customer ).isNotNull();
        assertThat(customer.getCustomerId()).isEqualTo(customerDto.id());
        assertThat(customer.getFirstName()).isEqualTo(customerDto.firstName());
        assertThat(customer.getLastName()).isEqualTo(customerDto.lastName());
 
        Address address = customer.getAddress();
        assertThat(address.getStreet()).isEqualTo(addressDto.street());
        assertThat(address.getHouseNumber()).isEqualTo(Integer.valueOf(addressDto.houseNumber()));
        assertThat(address.getZipcode()).isEqualTo(addressDto.zipcode());
        assertThat(address.getCity()).isEqualTo(addressDto.city());
 
    }
 
}


Mapping Between API and DTO

Create Mapper

The API model looks a bit different than the CustomerDto because it has no Address object and number and postalCode have different names in the CustomerDto.

Java
 
public class Customer {
 
  private String firstName;
 
  private String lastName;
 
  private String street;
 
  private String number;
 
  private String postalCode;
 
  private String city;
  // Getters and setters left out for brevity
}


In order to create a mapper, you need to add a bit more @Mapping annotations, just like you did before for the customer ID.

Java
 
@Mapper(componentModel = "spring")
public interface CustomerPortMapper {
 
    @Mapping(source = "street", target = "address.street")
    @Mapping(source = "number", target = "address.houseNumber")
    @Mapping(source = "postalCode", target = "address.zipcode")
    @Mapping(source = "city", target = "address.city")
    CustomerDto transformToCustomerDto(Customer customerApi);
 
    @Mapping(source = "id", target = "customerId")
    @Mapping(source = "address.street", target = "street")
    @Mapping(source = "address.houseNumber", target = "number")
    @Mapping(source = "address.zipcode", target = "postalCode")
    @Mapping(source = "address.city", target = "city")
    CustomerFullData transformToCustomerApi(CustomerDto customerDto);
 
}


Again, the generated CustomerPortMapperImpl class can be found in the target/generated-sources/annotations directory after invoking the Maven compile target.

Create Controller

The mapper is injected in the Controller and the corresponding mappers can easily be used.

Java
 
@RestController
class CustomerController implements CustomerApi {
 
    private final CustomerPortMapper customerPortMapper;
    private final CustomerService customerService;
 
    CustomerController(CustomerPortMapper customerPortMapper, CustomerService customerService) {
        this.customerPortMapper = customerPortMapper;
        this.customerService = customerService;
    }
 
    @Override
    public ResponseEntity<CustomerFullData> createCustomer(Customer customerApi) {
 
        CustomerDto customerDtoIn = customerPortMapper.transformToCustomerDto(customerApi);
        CustomerDto customerDtoOut = customerService.createCustomer(customerDtoIn);
 
        return ResponseEntity.ok(customerPortMapper.transformToCustomerApi(customerDtoOut));
    }
 
    @Override
    public ResponseEntity<CustomerFullData> getCustomer(Long customerId) {
        CustomerDto customerDtoOut = customerService.getCustomer(customerId);
        return ResponseEntity.ok(customerPortMapper.transformToCustomerApi(customerDtoOut));
    }
 
}


Test Mapper

A unit test is created in a similar way as the one for the Service and can be viewed here.

In order to test the complete application, an integration test is created for creating a customer.

Java
 
@SpringBootTest
@AutoConfigureMockMvc
class CustomerControllerIT {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    void whenCreateCustomer_thenReturnOk() throws Exception {
        String body = """
                {
                  "firstName": "John",
                  "lastName": "Doe",
                  "street": "street",
                  "number": "42",
                  "postalCode": "1234",
                  "city": "city"
                }
                """;
 
 
        mockMvc.perform(post("/customer")
                        .contentType("application/json")
                        .content(body))
                .andExpect(status().isOk())
                .andExpect(jsonPath("firstName", equalTo("John")))
                .andExpect(jsonPath("lastName", equalTo("Doe")))
                .andExpect(jsonPath("customerId", equalTo(0)))
                .andExpect(jsonPath("street", equalTo("street")))
                .andExpect(jsonPath("number", equalTo("42")))
                .andExpect(jsonPath("postalCode", equalTo("1234")))
                .andExpect(jsonPath("city", equalTo("city")));
 
    }
 
}


Conclusion

MapStruct is an easy-to-use library for mapping between models. If the basic mapping is not sufficient, you are even able to create your own custom mapping logic (which is not demonstrated in this blog). It is advised to read the official documentation to get a comprehensive list of all available features.

API Data transfer object Java (programming language) Object (computer science) Strings

Published at DZone with permission of Gunter Rotsaert, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Writing DTOs With Java8, Lombok, and Java14+
  • User-Friendly API Publishing and Testing With Retrofit
  • Hibernate Validator vs Regex vs Manual Validation: Which One Is Faster?
  • Functional Approach To String Manipulation in Java

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!