Spring Boot MapStruct Example

In a previous couple of tutorials, we created a Spring boot project, built CRUD Restful web services with DTO, and used the ModelMapper library to convert the JPA entity into DTO and vice versa.

Refer to previous tutorials:

Spring Boot 3 CRUD RESTful WebServices with MySQL Database

Spring Boot DTO Example Tutorial

Spring Boot ModelMapper Example - Map Entity to DTO

In this tutorial, we will learn how to use the MapStruct library to map the JPA entity into DTO and vice versa in the Spring boot application.

MapStruct Library Overview

The MapStruct is an annotation-based code generator/mapper which greatly simplifies the mapping implementations of Java Beans. It follows convention over configuration and uses plain method invocations. MapStruct operations are very fast, type-safe, and easy to understand.

MapStruct automates the process of creating a mapper to map data objects with model objects using annotations. It creates a mapper implementation at compile time which helps the developer to figure out errors during development and makes it easy to understand.

Check the official doc to read more about MapStruct at https://mapstruct.org/

Prerequisites

This tutorial is a continuation of below three tutorials so first, create CRUD REST APIs using below tutorials:

Spring Boot 3 CRUD RESTful WebServices with MySQL Database

Spring Boot DTO Example Tutorial

Spring Boot ModelMapper Example - Map Entity to DTO

The complete source code of this tutorial is available on my GitHub repository at Spring Boot CRUD RESTful WebServices

Development Steps

If you want to use the MapStruct library in your existing Spring boot project then follow these simple steps:
1. Add Maven Dependencies
2. User and UserDto Classes
3. Create UserMapper
4. Use UserMapper in Service Class to map the JPA entity into DTO and vice versa.
5. Test CRUD REST APIs using the Postman client

1. Add Maven Dependencies

Open the pom.xml file and add below Maven dependency and plugin.

MapStruct maven dependency:
		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct</artifactId>
			<version>${org.mapstruct.version}</version>
		</dependency>

Let’s 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.8.1</version>
	<configuration>
		<source>17</source>
		<target>17</target>
		<annotationProcessorPaths>
			<path>
				<groupId>org.mapstruct</groupId>
				<artifactId>mapstruct-processor</artifactId>
				<version>${org.mapstruct.version}</version>
			</path>
			<path>
				<groupId>org.projectlombok</groupId>
				<artifactId>lombok</artifactId>
				<version>${org.projectlombok.version}</version>
			</path>
			<path>
				<groupId>org.projectlombok</groupId>
				<artifactId>lombok-mapstruct-binding</artifactId>
				<version>${lombok-mapstruct-binding.version}</version>
			</path>
		</annotationProcessorPaths>
	</configuration>
</plugin>
Note that we have added below annotation processor path to the above plugin to support MapStruct annotations with Lombok annotations:

			<path>
				<groupId>org.projectlombok</groupId>
				<artifactId>lombok</artifactId>
				<version>${org.projectlombok.version}</version>
			</path>
			<path>
				<groupId>org.projectlombok</groupId>
				<artifactId>lombok-mapstruct-binding</artifactId>
				<version>${lombok-mapstruct-binding.version}</version>
			</path>
Next, add the version properties to the properties section:
	<properties>
		<java.version>17</java.version>
		<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
		<org.projectlombok.version>1.18.20</org.projectlombok.version>
		<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
	</properties>

2. User JPA Entity and UserDto

Here is our User JPA entity:
package net.javaguides.springboot.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String firstName;
    @Column(nullable = false)
    private String lastName;
    @Column(nullable = false, unique = true)
    private String email;
}

Here is our UserDto class:
package net.javaguides.springboot.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
}

3. Create UserMapper

Next, let's create a Mapper using MapStruct. Let's create an AutoUserMapper interface and define the mapping methods to map Entity to DTO and vice versa.

package net.javaguides.springboot.mapper;

import net.javaguides.springboot.dto.UserDto;
import net.javaguides.springboot.entity.User;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface AutoUserMapper {

    AutoUserMapper MAPPER = Mappers.getMapper(AutoUserMapper.class);

    UserDto mapToUserDto(User user);

    User mapToUser(UserDto userDto);
}
Notice we did not create an implementation class for our AutoUserMapper interface because MapStruct creates it for us during compilation time.

The @Mapper annotation marks the interface as a mapping interface and lets the MapStruct processor kick in during compilation.

We defined two mapping methods to convert JPA entity into DTO and vice versa:

    UserDto mapToUserDto(User user);

    User mapToUser(UserDto userDto);
An instance of the interface implementation can be retrieved from the Mappers class:

    AutoUserMapper MAPPER = Mappers.getMapper(AutoUserMapper.class);

Mapping Fields With Different Field Names

From our previous example, MapStruct was able to map our beans automatically because they have the same field names. So, what if a bean we are about to map has a different field name?.

Consider email field name is different in both User and UserDto. For example:
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String firstName;
    @Column(nullable = false)
    private String lastName;
    @Column(nullable = false, unique = true)
    private String email;
}
The email field name in UserDto is emailAddress:
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    private Long id;
    private String firstName;
    private String lastName;
    private String emailAddress;
}
When mapping different field names, we will need to configure its source field to its target field, and to do that, we will need to add @Mapping annotation for each field. 

For example:
@Mapper
public interface AutoUserMapper {

    AutoUserMapper MAPPER = Mappers.getMapper(AutoUserMapper.class);

    @Mapping(source = "email", target = "emailAddress")
    UserDto mapToUserDto(User user);

    @Mapping(source = "emailAddress", target = "email")
    User mapToUser(UserDto userDto);
}

3. Use UserMapper in Service Class to map the JPA entity into DTO and vice versa

package net.javaguides.springboot.service.impl;

import lombok.AllArgsConstructor;
import net.javaguides.springboot.dto.UserDto;
import net.javaguides.springboot.entity.User;
import net.javaguides.springboot.mapper.AutoUserMapper;
import net.javaguides.springboot.mapper.UserMapper;
import net.javaguides.springboot.repository.UserRepository;
import net.javaguides.springboot.service.UserService;
import org.apache.logging.log4j.util.Strings;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;

    private ModelMapper modelMapper;

    @Override
    public UserDto createUser(UserDto userDto) {

        // Convert UserDto into User JPA Entity
       // User user = UserMapper.mapToUser(userDto);

        //User user = modelMapper.map(userDto, User.class);

        User user = AutoUserMapper.MAPPER.mapToUser(userDto);

        User savedUser = userRepository.save(user);

        // Convert User JPA entity to UserDto
        //UserDto savedUserDto = UserMapper.mapToUserDto(savedUser);

        //UserDto savedUserDto = modelMapper.map(savedUser, UserDto.class);

        UserDto savedUserDto = AutoUserMapper.MAPPER.mapToUserDto(savedUser);

        return savedUserDto;
    }

    @Override
    public UserDto getUserById(Long userId) {
        User user = userRepository.findById(userId).get();
        //return UserMapper.mapToUserDto(user);
        //return modelMapper.map(user, UserDto.class);
        return AutoUserMapper.MAPPER.mapToUserDto(user);
    }

    @Override
    public List<UserDto> getAllUsers() {
        List<User> users = userRepository.findAll();
//        return users.stream().map(UserMapper::mapToUserDto)
//                .collect(Collectors.toList());

//        return users.stream().map((user) -> modelMapper.map(user, UserDto.class))
//                .collect(Collectors.toList());

        return users.stream().map((user) -> AutoUserMapper.MAPPER.mapToUserDto(user))
                .collect(Collectors.toList());
    }

    @Override
    public UserDto updateUser(UserDto user) {

        User existingUser = userRepository.findById(user.getId()).get()

        existingUser.setFirstName(user.getFirstName());
        existingUser.setLastName(user.getLastName());
        existingUser.setEmail(user.getEmail());
        User updatedUser = userRepository.save(existingUser);
        //return UserMapper.mapToUserDto(updatedUser);
        //return modelMapper.map(updatedUser, UserDto.class);
        return AutoUserMapper.MAPPER.mapToUserDto(updatedUser);
    }

    @Override
    public void deleteUser(Long userId) {

        userRepository.deleteById(userId);
    }
}

4. Test CRUD REST APIs using the Postman Client

Create User REST API:

HTTP Method: POST
Request Body:
{
    "firstName": "ramesh",
    "lastName":"fadatare",
    "email": "[email protected]"
}
Refer to this screenshot to test Create User REST API:

Get User REST API:

HTTP Method: GET

Refer to this screenshot to test GET User REST API:

Update User REST API:

HTTP Method: PUT
Request Body:
{
    "firstName": "ram",
    "lastName":"fadatare",
    "email": "[email protected]"
}
Refer to this screenshot to test the Update User REST API:

Get All Users REST API:

HTTP Method: GET

Refer to this screenshot to test GET All User REST API:

DELETE User REST API:

HTTP Method: DELETE

Refer to this screenshot to test Delete User REST API:

Source Code on GitHub

The source code of this tutorial is available on my GitHub repository at Spring Boot CRUD RESTful WebServices

Conclusion

In this tutorial, we have seen how to use the MapStruct library to map the JPA entity into DTO and vice versa in the Spring boot application.

Comments