Deep dive into Mapstruct @ Spring
By: Miguel Duque
Tags: Java, Mapstruct, Mapping Tool, Spring Boot, Spring

Basic Mappings

Let’s start our application with a basic model, which contains the class Doctor. Our service will retrieve this class from the model layer and then return a DoctorDto class.

Model class:

@Data

public class Doctor {

private int id;

private String name;

}

Dto class:

@Data

public class DoctorDto {

private int id;

private String name;

}

To do this, we should create our Mapper interface:

@Mapper(componentModel = "spring")

public interface DoctorMapper {

DoctorDto toDto(Doctor doctor);

}

As both classes have the same property names (id and name), mapstruct will include the mapping of both fields in the generated class:

@Component

public class DoctorMapperImpl implements DoctorMapper {



@Override

public DoctorDto toDto(Doctor doctor) {

if ( doctor == null ) {

return null;

}



DoctorDto doctorDto = new DoctorDto();



doctorDto.setId( doctor.getId() );

doctorDto.setName( doctor.getName() );



return doctorDto;

}

}

By adding componentModel = “spring”, the generated mapper will be a Spring bean and can be retrieved with the @Autowired annotation like any other bean:

@Service

public class DoctorService {



private final DoctorMapper doctorMapper;

private final DoctorRepository doctorRepository;



@Autowired

public DoctorService(DoctorMapper doctorMapper) {

this.doctorMapper = doctorMapper;

this.doctorRepository = doctorRepository;

}



public DoctorDto getDoctor(Integer id) {

Doctor doctor = doctorRepository.findById(id);

return doctorMapper.toDto(doctor);

}

}

Mapping Different Property Name

If we include a property phone in the Doctor class:

@Data

public class Doctor {

private int id;

private String name;

private String phone;

}

To be mapped to contact in DoctorDto:

@Data

public class DoctorDto {

private int id;

private String name;

private String contact;

}

Our mapper won't be able to map it automatically. To do this, we will need to create a rule for this mapping:

@Mapper(componentModel = "spring")

public interface DoctorMapper {



@Mapping(source = "phone", target = "contact")

DoctorDto toDto(Doctor doctor);

}

Making sure every property is mapped

If we want to guarantee that we don't forget to map any target property, we can configure the unmappedTargetPolicy option on our mapper:

@Mapper(

componentModel = "spring",

unmappedTargetPolicy = ReportingPolicy.ERROR)

public interface DoctorMapper {



@Mapping(source = "phone", target = "contact")

DoctorDto toDto(Doctor doctor);

}

With this configuration, if we remove

@Mapping(source = "phone", target = "contact")

our code will fail during compilation with the error:

Unmapped target property: “contact”. DoctorDto toDto(Doctor doctor);

If for some reason, we want to ignore a target property, we can add:

@Mapping(target = "contact", ignore = true)

Similarly, we can also guarantee that all source properties are mapped by configuring the unmappedSourcePolicy option.

Mapping Child Entity property

Most of the time, the class that we need to map contains child objects. For example:

@Data

public class Doctor {

private int id;

private String name;

private String phone;

private Speciality speciality;

} @Data

public class Speciality {

private int id;

private String name;

}

And in our Dto, instead of the full speciality, we just need its name:

@Data

public class DoctorDto {

private int id;

private String name;

private String contact;

private String specialityName;

}

This situation is also straightforward with mapstruct:

@Mapping(source = "phone", target = "contact")

@Mapping(source = "speciality.name", target = "specialityName")

DoctorDto toDto(Doctor doctor);

Mapping the full Child Entity — Using another mapper

As before, our Doctor class has a child object Address:

@Data

public class Doctor {

private int id;

private String name;

private String phone;

private Speciality speciality;

private Address address;

}

But in this case, we want to map it to a new object in our DoctorDto class:

@Data

public class DoctorDto {

private int id;

private String name;

private String contact;

private String specialityName;

private AddressDto address;

}

To perform the mapping between Address and AddressDto class, we should create a different mapper interface:

@Mapper(componentModel = "spring")

public interface AddressMapper {

AddressDto toDto(Address address);

}

Then, in our DoctorMapper we should make sure that this mapper is used when mapping the Doctor to DoctorDto. This can be done with the "uses" option on the mapper configuration:

@Mapper(

componentModel = "spring",

unmappedTargetPolicy = ReportingPolicy.ERROR,

uses = {AddressMapper.class})

public interface DoctorMapper {



@Mapping(source = "phone", target = "contact")

@Mapping(source = "speciality.name", target = "specialityName")

DoctorDto toDto(Doctor doctor);

}

We will see that our DoctorMapperImpl will Autowire and use our AddressMapper:

@Component

public class DoctorMapperImpl implements DoctorMapper {



@Autowired

private AddressMapper addressMapper;



@Override

public DoctorDto toDto(Doctor doctor) {

if ( doctor == null ) {

return null;

}



DoctorDto doctorDto = new DoctorDto();



doctorDto.setSpecialityName( doctorSpecialityName(doctor));

doctorDto.setContact( doctor.getPhone() );

doctorDto.setId( doctor.getId() );

doctorDto.setName( doctor.getName() );

doctorDto.setAddress(

addressMapper.toDto( doctor.getAddress() ) );



return doctorDto;

}



...

}

Mapping with a custom method

Let's now add a list of patients to our Doctor class:

@Data

public class Doctor {

private int id;

private String name;

private String phone;

private Speciality speciality;

private Address address;

private List<Patient> patients;

}

However, on our DoctorDto we only want the number of patients:

@Data

public class DoctorDto {

private int id;

private String name;

private String contact;

private String specialityName;

private AddressDto address;

private int numPatients;

}

This mapping requires 2 things:

A custom method with the @Named annotation

The qualifiedByName config on the Mapping annotation

@Mapper(

componentModel = "spring",

unmappedTargetPolicy = ReportingPolicy.ERROR,

uses = {AddressMapper.class})

public interface DoctorMapper {



@Mapping(source = "phone", target = "contact")

@Mapping(source = "speciality.name", target = "specialityName")

@Mapping(

source = "patients",

target = "numPatients",

qualifiedByName = "countPatients")

DoctorDto toDto(Doctor doctor);



@Named("countPatients")

default int getNumPatients(List<Patient> patients) {

if(patients == null) {

return 0;

}

return patients.size();

}

}

@BeforeMapping @AfterMapping

The previous example (mapping from List<Patient> patients to int numPatients) can also be done with @BeforeMapping and @AfterMapping

@Mapper(

componentModel = "spring",

unmappedTargetPolicy = ReportingPolicy.ERROR,

uses = {AddressMapper.class})

public interface DoctorMapper {



@BeforeMapping

default void setList(Doctor doctor) {

if (doctor != null && doctor.getPatients() == null) {

doctor.setPatients(new ArrayList<>());

}

}



@Mapping(source = "phone", target = "contact")

@Mapping(source = "speciality.name", target = "specialityName")

@Mapping(target = "numPatients", ignore = true)

DoctorDto toDto(Doctor doctor);



@AfterMapping

default void setNumPatients(Doctor doctor,

@MappingTarget DoctorDto doctorDto) {

doctorDto.setNumPatients(doctor.getPatients().size());

}

}

These methods will be called in the beginning and end of our generated mapping method:

@Override

public DoctorDto toDto(Doctor doctor) {

setList( doctor );



if ( doctor == null ) {

return null;

}



DoctorDto doctorDto = new DoctorDto();



doctorDto.setSpecialityName( doctorSpecialityName( doctor ) );

doctorDto.setContact( doctor.getPhone() );

doctorDto.setId( doctor.getId() );

doctorDto.setName( doctor.getName() );

doctorDto.setAddress( addressMapper.toDto(doctor.getAddress()));



setNumPatients( doctor, doctorDto );



return doctorDto;

}

Mapping with an additional parameter

Let's check how to handle a situation where your mapper needs to receive an additional parameter, besides your Entity.

In this case, your Doctor class can only get the City Id:

@Data

public class Doctor {

private int id;

private String name;

private String phone;

private Speciality speciality;

private Address address;

private List<Patient> patients;

private int cityId;

}

But needs to map it to the City Name:

@Data

public class DoctorDto {

private int id;

private String name;

private String contact;

private String specialityName;

private AddressDto address;

private int numPatients;

private String cityName;

}

Your service will fetch the list of cities and pass them to the mapper

@Service

public class DoctorService {



private final DoctorMapper doctorMapper;

private final DoctorRepository doctorRepository;



@Autowired

public DoctorService(DoctorMapper doctorMapper) {

this.doctorMapper = doctorMapper;

this.doctorRepository = doctorRepository;

}



public DoctorDto getDoctor(Integer id) {

Doctor doctor = doctorRepository.findById(id);

List<City> cities = getCities();

return doctorMapper.toDto(doctor, cities);

}

}

In our mapper, we need to:

mark the additional parameter (list of cities) with @Context annotation

create a custom method to handle our mapping

request the context parameter (list of cities) on our custom mapping method

@Mapping(source = "phone", target = "contact")

@Mapping(source = "speciality.name", target = "specialityName")

@Mapping(target = "numPatients", ignore = true)

@Mapping(source = "cityId",

target = "cityName",

qualifiedByName = "cityName")

DoctorDto toDto(Doctor doctor, @Context List<City> cities);



@Named("cityName")

default String getCityName(int cityId, @Context List<City> cities) {

return cities.stream()

.filter(city -> city.getId() == cityId)

.findAny()

.map(City::getName)

.orElse(null);

}

Dependency injection on mapping methods

You will probably find yourself in situations where your custom mapping methods require another bean (another mapper, a repository, a service, etc).

In these situations, you will need to Autowire that bean to your mapper, so let's see an example of how to do it.

In this example, our Patient class will be an abstract class:

@Data

public abstract class Patient {

private int id;

private String name;

private int age;

}

Which contains two implementations:

public class Man extends Patient {

} public class Woman extends Patient {

}

As we previously saw, this is the state of our Doctor entity:

@Data

public class Doctor {

private int id;

private String name;

private String phone;

private Speciality speciality;

private Address address;

private List<Patient> patients;

private int cityId;

}

However, our PatientDto requires a distinct list for each concrete class:

@Data

public class DoctorDto {

private int id;

private String name;

private String contact;

private String specialityName;

private AddressDto address;

private int numPatients;

private String cityName;

private PatientsDto patients;

}

And

@Data

public class PatientsDto {

private List<ManDto> men = new ArrayList<>();

private List<WomanDto> women = new ArrayList<>();



public void addMan(ManDto manDto) {

men.add(manDto);

}



public void addWoman(WomanDto womanDto) {

women.add(womanDto);

}

}

So to map these concrete classes, we should start by creating two mappers:

@Mapper(componentModel = "spring")

public interface WomanMapper {

WomanDto toDto(Woman woman);

} @Mapper(componentModel = "spring")

public interface ManMapper {

ManDto toDto(Man man);

}

But to map from List<Patient> patients to PatientsDto patients we also need a different mapper that uses the newly created mappers (WomanMapper and ManMapper), that will end up being used by our DoctorMapper.

Since our PatientsMapper will need to use the WomanMapper and ManMapper, instead of creating an Interface, we need to create an Abstract class:

@Mapper(componentModel = "spring")

public abstract class PatientsMapper {



@Autowired

private ManMapper manMapper;



@Autowired

private WomanMapper womanMapper;



public PatientsDto toDto(List<Patient> patients) {

PatientsDto patientsDto = new PatientsDto();

for (Patient patient : patients) {

if (patient instanceof Man) {

patientsDto.addMan(

manMapper.toDto((Man) patient));

} else if (patient instanceof Woman) {

patientsDto.addWoman(

womanMapper.toDto((Woman) patient));

}

}

return patientsDto;

}

}

Finally, to make our DoctorMapper use the PatientsMapper, we need to add some configuration:

@Mapper(

componentModel = "spring",

unmappedTargetPolicy = ReportingPolicy.ERROR,

uses = {AddressMapper.class, PatientsMapper.class})

public interface DoctorMapper {

...

}

As the patients variable has the same name on the Entity and Dto classes, we don't need to specify anything else.

This will be the end result of the generated classes:

@Component

public class DoctorMapperImpl implements DoctorMapper {



@Autowired

private AddressMapper addressMapper;

@Autowired

private PatientsMapper patientsMapper;



@Override

public DoctorDto toDto(Doctor doctor, List<City> cities) {

if (doctor == null) {

return null;

}



DoctorDto doctorDto = new DoctorDto();



doctorDto.setNumPatients(

getNumPatients(doctor.getPatients()));

doctorDto.setCityName(

getCityName(doctor.getCityId(), cities));

doctorDto.setSpecialityName(doctorSpecialityName(doctor));

doctorDto.setContact(doctor.getPhone());

doctorDto.setId(doctor.getId());

doctorDto.setName(doctor.getName());

doctorDto.setAddress(

addressMapper.toDto(doctor.getAddress()));

doctorDto.setPatients(

patientsMapper.toDto(doctor.getPatients()));



return doctorDto;

}

...

} @Component

public class ManMapperImpl implements ManMapper {



@Override

public ManDto toDto(Man man) {

if ( man == null ) {

return null;

}



ManDto manDto = new ManDto();



manDto.setId( man.getId() );

manDto.setName( man.getName() );

manDto.setAge( man.getAge() );



return manDto;

}

} @Component

public class WomanMapperImpl implements WomanMapper {



@Override

public WomanDto toDto(Woman woman) {

if ( woman == null ) {

return null;

}



WomanDto womanDto = new WomanDto();



womanDto.setId( woman.getId() );

womanDto.setName( woman.getName() );

womanDto.setAge( woman.getAge() );



return womanDto;

}

}

Updates

Mapstruct also provides an easy way to handle updates. If we want to update our Doctor entity with the information on DoctorDto, we just need to create:

@Mapping(source = "contact", target = "phone")

@Mapping(source = "specialityName", target = "speciality.name")

void updateEntity(DoctorDto doctorDto,

@MappingTarget Doctor doctor);

As we can see on the generated implementation, it will map all variables (null or not null):

@Override

public void updateEntity(DoctorDto doctorDto, Doctor doctor) {

if ( doctorDto == null ) {

return;

}



if ( doctor.getSpeciality() == null ) {

doctor.setSpeciality( new Speciality() );

}

doctorDtoToSpeciality( doctorDto, doctor.getSpeciality() );

doctor.setPhone( doctorDto.getContact() );

doctor.setId( doctorDto.getId() );

doctor.setName( doctorDto.getName() );

if ( doctorDto.getAddress() != null ) {

if ( doctor.getAddress() == null ) {

doctor.setAddress( new Address() );

}

addressDtoToAddress( doctorDto.getAddress(), doctor.getAddress() );

}

else {

doctor.setAddress( null );

}

}

Patch Updates

As we have seen in the previous example, the default update method will map every property, even if it is null. So if you encounter yourself in a situation where you just want to perform a patch update (only update the not null values), you need to use the nullValuePropertyMappingStrategy:

@BeanMapping(nullValuePropertyMappingStrategy =

NullValuePropertyMappingStrategy.IGNORE)

@Mapping(source = "contact", target = "phone")

@Mapping(source = "specialityName", target = "speciality.name")

void updatePatchEntity(DoctorDto doctorDto,

@MappingTarget Doctor doctor);

The generated method will perform null checks before updating the values:

@Override

public void updatePatchEntity(DoctorDto doctorDto, Doctor doctor) {

if ( doctorDto == null ) {

return;

}



if ( doctorDto.getContact() != null ) {

doctor.setPhone( doctorDto.getContact() );

}

doctor.setId( doctorDto.getId() );

if ( doctorDto.getName() != null ) {

doctor.setName( doctorDto.getName() );

}

if ( doctorDto.getAddress() != null ) {

if ( doctor.getAddress() == null ) {

doctor.setAddress( new Address() );

}

addressDtoToAddress(

doctorDto.getAddress(),

doctor.getAddress() );

}

if ( doctor.getSpeciality() == null ) {

doctor.setSpeciality( new Speciality() );

}

doctorDtoToSpeciality1( doctorDto, doctor.getSpeciality() );

}

Conclusion

This article described how to take advantage of the Mapstruct library to significantly reduce our boilerplate code in a safe and elegant way.

As seen in the examples, Mapstruct offers a vast set of functionalities and configurations which allows us to create from basic to complex mappers in an easy and fast way.