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. We will use the JPA layer of a Spring Boot application to access your data.
MapStruct in a nutshell
In its simplest definition a DTO is a serializable object that allows the flow of information between application layers. To achieve that, you would typically need to define a Java Bean which acts as DTO 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.
Firstly, 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
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
Next, 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
As field names are identical between Entity and DTOs, that’s all you need to have automatic binding.
Finally, add the Repository class to our project:
@Repository public interface CustomerRepository extends JpaRepository <>
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 create( @Valid @RequestBody CustomerPostDto customerPostDto ) < customerRepository.save( mapstructMapper.customerPostDtoToCustomer(customerPostDto) ); return new ResponseEntity<>(HttpStatus.CREATED); > @GetMapping("/") public ResponseEntity 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:
org.springframework.boot spring-boot-starter-web 2.4.3 org.springframework.boot spring-boot-starter-data-jpa 2.4.3 org.springframework.boot spring-boot-starter-validation 2.4.3 com.h2database h2 runtime org.projectlombok lombok 1.18.16 provided org.mapstruct mapstruct 1.4.2.Final org.projectlombok lombok
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:
org.apache.maven.plugins maven-compiler-plugin 3.5.1 1.8 org.mapstruct mapstruct-processor 1.4.2.Final
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 "" http://localhost:8080/customers
And retrieving the Entity:
curl -s http://localhost:8080/customers/1 | jq
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 ); >