Mapping DTOs in Spring Boot with MapStruct

This tutorial covers how to use MapStruct library to map automatically your Data Transfer Objects with your repository data, which in this example is managed by the JPA layer of a Spring Boot application.

In its simplest definition a DTO is a serializable object that migrating between the various application layers allows the flow of information. To achieve that, you would typically need to define a Java Bean which acts as Data Transfer Object and a Mapper class which contains the logic to map the Bean with the Data.

Thanks to the MapStruct project, this can be simplified as you can get automatic mapping between the DTO and the Entity Bean by adding a simple interface. In the second part of this tutorial, we will show how to perform advanced mapping, in case the field names differ between the DTO and the Entity Bean.

We will start adding an Entity object named Customer:

package com.mapstruct.demo.entities;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Getter
@Setter
@Entity
@Table(name = "customer")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "native")
    @Column(name = "id", unique = true, nullable = false)
    private int id;

    @Basic
    @Column(name = "email", nullable = false, length = 250)
    private String email;

    @Basic
    @Column(name = "password", nullable = false, length = 50)
    private String password;

    @Basic
    @Column(name = "name", nullable = true, length = 50)
    private String name;

    @Basic
    @Column(name = "surname", nullable = true, length = 50)
    private String surname;
 
}

Please notice that we are using Lombok annotations to have also automatic mapping of @Getter @Setter fields.

Next, it’s time to create the DTO objects that will be used in a REST applications. For the sake of this example, we will create two DTO objects: one will be used to transfer data upon a GET request and another which will be used to transfer data following up to a POST request:

@Getter
@Setter
public class CustomerGetDto {
    @JsonProperty("id")
    private int id;

    @JsonProperty("email")
    private String email;

    @JsonProperty("name")
    private String name;

    @JsonProperty("surname")
    private String surname;
}


@Getter
@Setter
public class CustomerPostDto {
    @JsonProperty("id")
    private int id;

    @Email
    @NotNull
    @JsonProperty("email")
    private String email;

    @NotNull
    @JsonProperty("password")
    private String password;

    @JsonProperty("name")
    private String name;

    @JsonProperty("surname")
    private String surname;
}

Great. In order to bind the Entity objects with the actual DTO objects, we will use a simple Interface which includes a Mapper annotation in it:

@Mapper(
        componentModel = "spring"
)
public interface MapStructMapper {

    CustomerGetDto customerToCustomerGetDto(
            Customer customer
    );

    Customer customerPostDtoToCustomer(
            CustomerPostDto customerPostDTO
    );
}

As field names are identical between Entity and DTOs, that’s all you need to have automatic binding.

We will finish adding the Repository class to our project:

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Integer> {}

To allow users interact with our application, let’s add a Controller class with a POST and GET method to create and return the list of Customer objects:

@RestController
@RequestMapping("/customers")
public class CustomerController {

    private MapStructMapper mapstructMapper;

    private CustomerRepository customerRepository;

    @Autowired
    public CustomerController(
            MapStructMapper mapstructMapper,
            CustomerRepository userRepository
    ) {
        this.mapstructMapper = mapstructMapper;
        this.customerRepository = userRepository;
    }

    @PostMapping()
    public ResponseEntity<Void> create(
            @Valid @RequestBody CustomerPostDto customerPostDto
    ) {
        customerRepository.save(
                mapstructMapper.customerPostDtoToCustomer(customerPostDto)
        );

        return new ResponseEntity<>(HttpStatus.CREATED);
    }

    @GetMapping("/{id}")
    public ResponseEntity<CustomerGetDto> getById(
            @PathVariable(value = "id") int id
    ) {

        return new ResponseEntity<>(
                mapstructMapper.customerToCustomerGetDto(
                        customerRepository.findById(id).get()
                ),
                HttpStatus.OK
        );
    }
}

Complete the example adding an Application class to bootstrap the example:

package com.mapstruct.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import com.mapstruct.demo.controllers.CustomerController;

@SpringBootApplication
@EnableJpaRepositories("com.mapstruct.demo.repositories")
@EntityScan(basePackages = { "com.mapstruct.demo.entities" })

public class MapStructDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(MapStructDemoApplication.class, args);
	}

}

Building the Project

To build the example, we will add the required MapStruct and Lombok dependencies, plus the default SpringBoot starter libraries:

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
		<version>2.4.3</version>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
		<version>2.4.3</version>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-validation</artifactId>
		<version>2.4.3</version>
	</dependency>

	<dependency>
	    <groupId>com.h2database</groupId>
	    <artifactId>h2</artifactId>
	    <scope>runtime</scope>
	</dependency>

	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.16</version>
		<scope>provided</scope>
	</dependency>

	<dependency>
		<groupId>org.mapstruct</groupId>
		<artifactId>mapstruct</artifactId>
		<version>1.4.2.Final</version>
	</dependency>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
	</dependency>
</dependencies>

We will also add the annotationProcessorPaths section to the configuration part of the maven-compiler-plugin plugin. The mapstruct-processor is used to generate the mapper implementation during the build:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.4.2.Final</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

Running the application

You can run the Spring Boot application as usually with:

$ mvn clean install spring-boot:run

Next, you can test adding an Entity:

curl -i -X POST -H "Content-Type: application/json" -d "{\"email\":\"user@mail.com\",\"name\":\"john\",\"surname\":\"smith\",\"password\":\"secret\"}" http://localhost:8080/customers

And retrieving the Entity:

curl -s http://localhost:8080/customers/1 | jq
{
  "id": 1,
  "email": "user@mail.com",
  "name": "john",
  "surname": "smith"
}

Well done. We just managed to create, deploy and test a project which uses MapStruct

MapStruct generated Mapping

Behind the hoods, when you build the project, a MapStructMapperImpl is automatically generate to map the CustomerDTO fields with the Customer Entity objects:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-07-13T14:45:56+0200",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 11.0.4 (Oracle Corporation)"
)
@Component
public class MapStructMapperImpl implements MapStructMapper {

    @Override
    public CustomerGetDto customerToCustomerGetDto(Customer customer) {
        if ( customer == null ) {
            return null;
        }

        CustomerGetDto customerGetDto = new CustomerGetDto();

        customerGetDto.setId( customer.getId() );
        customerGetDto.setEmail( customer.getEmail() );
        customerGetDto.setName( customer.getName() );
        customerGetDto.setSurname( customer.getSurname() );

        return customerGetDto;
    }

    @Override
    public Customer customerPostDtoToCustomer(CustomerPostDto customerPostDTO) {
        if ( customerPostDTO == null ) {
            return null;
        }

        Customer customer = new Customer();

        customer.setId( customerPostDTO.getId() );
        customer.setEmail( customerPostDTO.getEmail() );
        customer.setPassword( customerPostDTO.getPassword() );
        customer.setName( customerPostDTO.getName() );
        customer.setSurname( customerPostDTO.getSurname() );

        return customer;
    }
}

As you can see, the mapping between source and target classes is straightforward. In some cases, however, the DTO fields can differ from the Repository fields. If this is the case, you can decorate the methods of the interface with the @Mappings annotation to define a custom source and target fields:

@Mapper(
        componentModel = "spring"
)

public interface MapStructMapper {

    @Mappings({
      @Mapping(target="Customerid", source="id")
    })
    CustomerGetDto customerToCustomerGetDto(
            Customer customer
    );

    @Mappings({
      @Mapping(target="Customerid", source="id")
    })
    Customer customerPostDtoToCustomer(
            CustomerPostDto customerPostDTO
    );
}

The source code for this example is available here: https://github.com/fmarchioni/masterspringboot/tree/master/jpa/mapstruct-demo

Exit mobile version