With microservices and distributed applications rapidly taking over the development landscape, data integrity and security are more important than ever. Secure communication channels and limited data transfer between these loosely coupled systems are paramount. Most of the time, end users or services do not need access to all the data in the model, but only to some specific parts.
Data Transfer Objects (DTOs) are often used in these applications.A DTO is simply an object that holds information that has been requested in another object. Typically, this information is a limited portion. For example, there are often interchanges between entities defined in the persistence layer and DTOs sent to the client. Since the DTO is a reflection of the original object, the mapper between these classes plays a key role in the conversion process.
This is the problem that MapStruct solves: creating bean mappers manually is very time consuming. But the library can automatically generate bean mapper classes.
In this article, we will dive into MapStruct.
MapStruct
MapStruct is an open source Java-based code generator for creating implementations of Java Bean conversion between the extended mapper . Using MapStruct, we only need to create interfaces , and the library will be annotated through the compilation process to automatically create a specific mapping implementation , greatly reducing the number of usually need to manually write the sample code .
MapStruct Dependencies
If you are using Maven, you can install MapStruct by introducing a dependency:
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
This dependency imports MapStruct’s core annotations. Since MapStruct works at compile time and will be integrated with build tools like Maven and Gradle, we must also add a plugin maven-compiler-plugin
to the tag and annotationProcessorPaths
to its configuration, which will generate the corresponding code at build time.
<build>
<plugins>
<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>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
It’s easier to install MapStruct if you’re using Gradle:
plugins {
id 'net.ltgt.apt' version '0.20'
}
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'
dependencies {
compile "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
net.ltgt.apt
The plugin will take care of handling the comments. You can enable the plugin apt-idea
or apt-eclipse
plugin depending on the IDE you are using.
The latest stable versions of MapStruct and its processors are available from the Maven central repository.
base map
We’ll start with some basic mappings. We’ll create a Doctor object and a DoctorDto. for convenience, they both use the same names for their property fields:
public class Doctor {
private int id;
private String name;
// getters and setters or builder
}
public class DoctorDto {
private int id;
private String name;
// getters and setters or builder
}
Now, in order to map between the two, we are going to create a DoctorMapper
interface. Use the @Mapper
annotation for this interface and MapStruct will know that this is a mapper between the two classes.
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
DoctorDto toDto(Doctor doctor);
}
This code creates an instance of the DoctorMapper
type INSTANCE
, which is the “entry point” for our call after generating the corresponding implementation code.
We define the toDto()
method in the interface, which takes a Doctor
instance as a parameter and returns a DoctorDto
instance. This is enough for MapStruct to know that we want to map a Doctor
instance to a DoctorDto
instance.
When we build/compile the application, the MapStruct annotation processor plugin recognizes the DoctorMapper interface and generates an implementation class for it.
public class DoctorMapperImpl implements DoctorMapper {
@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.id(doctor.getId());
doctorDto.name(doctor.getName());
return doctorDto.build();
}
}
DoctorMapperImpl
The class contains a toDto()
method that maps our Doctor
attribute value to the attribute field of DoctorDto
. To map a Doctor
instance to a DoctorDto
instance, you could write it like this:
DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor);
Note: You may have also noticed DoctorDtoBuilder
in the above implementation code. Because builder code tends to be long, the implementation code for the builder pattern is omitted here for brevity. If your class contains a Builder, MapStruct will try to use it to create an instance; if not, MapStruct will instantiate it via the new
keyword.
Mapping between different fields
Typically, the field names of the model and the DTO will not be exactly the same. The names may change slightly due to team members specifying their own naming and developers choosing different ways of packaging the returned information for different calling services.
MapStruct supports this case through the @Mapping
annotation.
Different property names
Let’s start by updating the Doctor
class to add a property specialty
:
public class Doctor {
private int id;
private String name;
private String specialty;
// getters and setters or builder
}
Add a specialization
property to the DoctorDto
class:
public class DoctorDto {
private int id;
private String name;
private String specialization;
// getters and setters or builder
}
Now, we need to let DoctorMapper
know about the inconsistency here. We can use the @Mapping
annotation and set its internal source
and target
tokens to point to the two fields that are inconsistent.
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
The meaning of this annotation code is that the specialty
field in Doctor
corresponds to specialization
in the DoctorDto
class.
After compilation, the following implementation code is generated:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public DoctorDto toDto(Doctor doctor) {
if (doctor == null) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.specialization(doctor.getSpecialty());
doctorDto.id(doctor.getId());
doctorDto.name(doctor.getName());
return doctorDto.build();
}
}
Multiple source classes
Sometimes a single class is not enough to build a DTO, and we may want to aggregate values from multiple classes into a single DTO for end users. This can also be done by setting appropriate flags in the @Mapping
annotation.
Let’s start by creating another new object Education
:
public class Education {
private String degreeName;
private String institute;
private Integer yearOfPassing;
// getters and setters or builder
}
Then add a new field to DoctorDto
:
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
// getters and setters or builder
}
Next, update the DoctorMapper
interface to the following code:
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.specialty", target = "specialization")
@Mapping(source = "education.degreeName", target = "degree")
DoctorDto toDto(Doctor doctor, Education education);
}
We added another @Mapping
annotation and set its source
to degreeName
for the Education
class and target
to the degree
field for the DoctorDto
class.
If the Education
class and the Doctor
class contain fields with the same name, we have to let the mapper know which one to use or it will throw an exception. For example, if both models contain a id
field, we have to choose which class maps id
to the DTO attribute.
subobject mapping
In most cases, POJOs will not contain only basic data types, which will often contain other classes. For example, a Doctor
class will have multiple patient classes in it:
public class Patient {
private int id;
private String name;
// getters and setters or builder
}
Add a list of patients to Doctor List
:
public class Doctor {
private int id;
private String name;
private String specialty;
private List<Patient> patientList;
// getters and setters or builder
}
Because Patient
needs to be converted, create a corresponding DTO for it:
public class PatientDto {
private int id;
private String name;
// getters and setters or builder
}
Finally, a new list is added to DoctorDto
that stores PatientDto
:
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
private List<PatientDto> patientDtoList;
// getters and setters or builder
}
Before modifying DoctorMapper
, we create a mapper interface that supports Patient
and PatientDto
conversions:
@Mapper
public interface PatientMapper {
PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
PatientDto toDto(Patient patient);
}
This is a basic mapper that will only handle a few basic data types.
Then, we’ll modify DoctorMapper
to handle the patient list:
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
Since we’re dealing with another class that needs to be mapped, the uses
flag of the @Mapper
annotation is set here so that @Mapper
can now use another @Mapper
mapper. We’ve only added one here, but you can add as many classes/mappers here as you want.
We’ve added the uses
flag, so when generating the mapper implementation for the DoctorMapper
interface, MapStruct will also convert the Patient
model to PatientDto
– since we’ve registered PatientMapper
for this task.
Compile to see the latest code you want to implement:
public class DoctorMapperImpl implements DoctorMapper {
private final PatientMapper patientMapper = Mappers.getMapper( PatientMapper.class );
@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.patientDtoList( patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.specialization( doctor.getSpecialty() );
doctorDto.id( doctor.getId() );
doctorDto.name( doctor.getName() );
return doctorDto.build();
}
protected List<PatientDto> patientListToPatientDtoList(List<Patient> list) {
if ( list == null ) {
return null;
}
List<PatientDto> list1 = new ArrayList<PatientDto>( list.size() );
for ( Patient patient : list ) {
list1.add( patientMapper.toDto( patient ) );
}
return list1;
}
}
Obviously, in addition to the toDto()
mapping method, a new mapping method was added to the final implementation – patientListToPatientDtoList()
. This method was added without explicitly defining it, simply because we added PatientMapper
to DoctorMapper
.
This method iterates through a list of Patient
, converts each element to a PatientDto
, and adds the converted object to the list inside the DoctorDto
object.
Updating existing instances
Sometimes we want to update a property in a model with the latest value of a DTO, using the @MappingTarget
annotation on the target object ( DoctorDto
in our case) will update the existing instance.
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
Regenerating the implementation code gives you the updateModel()
method:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public void updateModel(DoctorDto doctorDto, Doctor doctor) {
if (doctorDto == null) {
return;
}
if (doctor.getPatientList() != null) {
List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
if (list != null) {
doctor.getPatientList().clear();
doctor.getPatientList().addAll(list);
}
else {
doctor.setPatientList(null);
}
}
else {
List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
if (list != null) {
doctor.setPatientList(list);
}
}
doctor.setSpecialty(doctorDto.getSpecialization());
doctor.setId(doctorDto.getId());
doctor.setName(doctorDto.getName());
}
}
It is worth noting that since the patient list is a sub-entity in this model, the patient list is also updated.
data type conversion
data type mapping
MapStruct supports data type conversions between source
and target
attributes. It also provides automatic conversions between basic types and their corresponding wrapper classes.
Automatic type conversion applies:
between basic types and their corresponding wrapper classes. For example,int
andInteger
,float
andFloat
,long
andLong
,boolean
andBoolean
, etc.
Between any base type and any wrapper class. For example,int
andlong
,byte
andInteger
, etc.
All basic types and wrapper classes withString
. For example,boolean
andString
,Integer
andString
,float
andString
, and so on.- between the enumeration and
String
.
Java large number types (java.math.BigInteger
,java.math.BigDecimal
) and between Java basic types (including their wrapper classes) andString
.- See the official MapStruct documentation for additional details.
Therefore, during the generation of the mapper code, MapStrcut handles the type conversion itself if it falls into any of the above cases between the source and target fields.
We modify PatientDto
and add a new dateofBirth
field:
public class PatientDto {
private int id;
private String name;
private LocalDate dateOfBirth;
// getters and setters or builder
}
On the other hand, adding Patient
object has a dateOfBirth
of type String
:
public class Patient {
private int id;
private String name;
private String dateOfBirth;
// getters and setters or builder
}
Create a mapper between the two:
@Mapper
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
}
We can also use dateFormat
to set up formatting declarations when performing conversions on dates. The form of the generated implementation code is roughly as follows:
public class PatientMapperImpl implements PatientMapper {
@Override
public Patient toModel(PatientDto patientDto) {
if (patientDto == null) {
return null;
}
PatientBuilder patient = Patient.builder();
if (patientDto.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ofPattern("dd/MMM/yyyy")
.format(patientDto.getDateOfBirth()));
}
patient.id(patientDto.getId());
patient.name(patientDto.getName());
return patient.build();
}
}
As you can see, the date format declared by dateFormat
is used here. If we had not declared a format, MapStruct would have used the default format of LocalDate
, which is roughly as follows:
if (patientDto.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE
.format(patientDto.getDateOfBirth()));
}
Digital format conversion
As you can see in the above example, the format of the date can be specified with the dateFormat
flag when performing date conversions.
In addition to this, for conversion of numbers, the display format can be specified using numberFormat
:
@Mapping(source = "price", target = "price", numberFormat = "$#.00")
enumeration map (math.)
Enumeration mapping works in the same way as field mapping; MapStruct will map enumerations with the same name, which is fine. However, for enumerations with different names, we need to use the @ValueMapping
annotation. Again, this is similar to the @Mapping
annotation for normal types.
We’ll start by creating two enumerations. The first one is PaymentType
:
public enum PaymentType {
CASH,
CHEQUE,
CARD_VISA,
CARD_MASTER,
CARD_CREDIT
}
For example, this is a payment method available within the app, and now we’re going to create a more general, limited literacy map based on these options:
public enum PaymentTypeView {
CASH,
CHEQUE,
CARD
}
Now, we create the mapper interface between these two enum
:
@Mapper
public interface PaymentTypeMapper {
PaymentTypeMapper INSTANCE = Mappers.getMapper(PaymentTypeMapper.class);
@ValueMappings({
@ValueMapping(source = "CARD_VISA", target = "CARD"),
@ValueMapping(source = "CARD_MASTER", target = "CARD"),
@ValueMapping(source = "CARD_CREDIT", target = "CARD")
})
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
}
In this example, we set the general CARD
value, and the more specific CARD_VISA
, CARD_MASTER
and CARD_CREDIT
. There is a mismatch in the number of enumeration entries between the two enumerations – PaymentType
has 5 values, while PaymentTypeView
only has 3.
To build a bridge between these enumerations, we can use the @ValueMappings
annotation, which can contain multiple @ValueMapping
annotations. Here, we set source
to be one of the three concrete enumerations, and set target
to be CARD
.
MapStruct naturally handles these cases:
public class PaymentTypeMapperImpl implements PaymentTypeMapper {
@Override
public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
if (paymentType == null) {
return null;
}
PaymentTypeView paymentTypeView;
switch (paymentType) {
case CARD_VISA: paymentTypeView = PaymentTypeView.CARD;
break;
case CARD_MASTER: paymentTypeView = PaymentTypeView.CARD;
break;
case CARD_CREDIT: paymentTypeView = PaymentTypeView.CARD;
break;
case CASH: paymentTypeView = PaymentTypeView.CASH;
break;
case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + paymentType );
}
return paymentTypeView;
}
}
CASH
and CHEQUE
are converted to their corresponding values by default, and special CARD
values are handled by a switch
loop.
However, if you’re converting a lot of values to a more general value, this approach is a bit impractical. In fact, we don’t have to assign each value manually, we just need to have MapStruct take all the remaining available enumeration items (those that can’t be found with the same name in the target enumeration) and convert them directly to another enumeration item that corresponds to them.
This can be accomplished at MappingConstants
:
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
In this example, after completing the default mapping, all remaining (unmatched) enumeration entries are mapped to CARD
:
@Override
public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
if ( paymentType == null ) {
return null;
}
PaymentTypeView paymentTypeView;
switch ( paymentType ) {
case CASH: paymentTypeView = PaymentTypeView.CASH;
break;
case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;
break;
default: paymentTypeView = PaymentTypeView.CARD;
}
return paymentTypeView;
}
Another option is to use ANY UNMAPPED
:
@ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
In this way, MapStruct does not handle the default mapping first and then map the remaining enumeration items to target
values as it did in the previous section. Instead, MapStruct converts all values that are not explicitly mapped by the @ValueMapping
annotation to target
values.
set mapping (math.)
In short, using MapStruct handles collection mappings in the same way as simple types.
We create a simple interface or abstract class and declare the mapping method. MapStruct will automatically generate the mapping code based on our declaration. Typically, the generated code traverses the source collection, converts each element to the target type, and adds each converted element to the target collection.
List Mapping
We start by defining a new mapping method:
@Mapper
public interface DoctorMapper {
List<DoctorDto> map(List<Doctor> doctor);
}
The generated code is roughly as follows:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public List<DoctorDto> map(List<Doctor> doctor) {
if ( doctor == null ) {
return null;
}
List<DoctorDto> list = new ArrayList<DoctorDto>( doctor.size() );
for ( Doctor doctor1 : doctor ) {
list.add( doctorToDoctorDto( doctor1 ) );
}
return list;
}
protected DoctorDto doctorToDoctorDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setSpecialization( doctor.getSpecialization() );
return doctorDto;
}
}
As you can see, MapStruct automatically generates the mapping method from Doctor
to DoctorDto
for us.
However, note that if we add a new field fullName
to the DTO, an error will occur when generating the code:
Unmapped target property: "fullName".
Basically, this means that MapStruct cannot automatically generate mapping methods for us in the current case. Therefore, we need to manually define the mapping method between Doctor
and DoctorDto
. Refer to the previous subsection for details.
Set and Map mapping
Set and Map type data are handled similarly to List. Modify DoctorMapper
as follows:
@Mapper
public interface DoctorMapper {
Set<DoctorDto> setConvert(Set<Doctor> doctor);
Map<String, DoctorDto> mapConvert(Map<String, Doctor> doctor);
}
The generated final implementation code is as follows:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public Set<DoctorDto> setConvert(Set<Doctor> doctor) {
if ( doctor == null ) {
return null;
}
Set<DoctorDto> set = new HashSet<DoctorDto>( Math.max( (int) ( doctor.size() / .75f ) + 1, 16 ) );
for ( Doctor doctor1 : doctor ) {
set.add( doctorToDoctorDto( doctor1 ) );
}
return set;
}
@Override
public Map<String, DoctorDto> mapConvert(Map<String, Doctor> doctor) {
if ( doctor == null ) {
return null;
}
Map<String, DoctorDto> map = new HashMap<String, DoctorDto>( Math.max( (int) ( doctor.size() / .75f ) + 1, 16 ) );
for ( java.util.Map.Entry<String, Doctor> entry : doctor.entrySet() ) {
String key = entry.getKey();
DoctorDto value = doctorToDoctorDto( entry.getValue() );
map.put( key, value );
}
return map;
}
protected DoctorDto doctorToDoctorDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setSpecialization( doctor.getSpecialization() );
return doctorDto;
}
}
Similar to List mapping, MapStruct automatically generates a mapping method that converts Doctor
to DoctorDto
.
set mapping strategy
There are many scenarios where we need to convert data types that have a parent-child relationship. Typically, there will be a data type (parent) whose fields are a collection of another data type (child).
For this case, MapStruct provides a way to choose how to set or add the subtype to the parent type. Specifically, it is the collectionMappingStrategy
attribute in the @Mapper
annotation, which can take the values ACCESSOR_ONLY
, SETTER_PREFERRED
, ADDER_PREFERRED
or TARGET_IMMUTABLE
.
Each of these values indicates a different way of assigning values to a subtype collection. The default value is ACCESSOR_ONLY
, which means that subcollections can only be set using accessors.
This option comes in handy when the Collection field setter
method in the parent type is not available, but we have a subtype add
method; another useful case is when the Collection field in the parent type is immutable.
We create a new class:
public class Hospital {
private List<Doctor> doctors;
// getters and setters or builder
}
Also define a mapping target DTO class along with getter, setter and adder for subtype collection fields:
public class HospitalDto {
private List<DoctorDto> doctors;
public List<DoctorDto> getDoctors() {
return doctors;
}
public void setDoctors(List<DoctorDto> doctors) {
this.doctors = doctors;
}
public void addDoctor(DoctorDto doctorDTO) {
if (doctors == null) {
doctors = new ArrayList<>();
}
doctors.add(doctorDTO);
}
}
Create the corresponding mapper:
@Mapper(uses = DoctorMapper.class)
public interface HospitalMapper {
HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class);
HospitalDto toDto(Hospital hospital);
}
The generated final implementation code is:
public class HospitalMapperImpl implements HospitalMapper {
@Override
public HospitalDto toDto(Hospital hospital) {
if ( hospital == null ) {
return null;
}
HospitalDto hospitalDto = new HospitalDto();
hospitalDto.setDoctors( doctorListToDoctorDtoList( hospital.getDoctors() ) );
return hospitalDto;
}
}
As you can see, by default the strategy used is ACCESSOR_ONLY
, which uses the setter method setDoctors()
to write list data to the HospitalDto
object.
Relatively, if you use ADDER_PREFERRED
as the mapping policy:
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
uses = DoctorMapper.class)
public interface HospitalMapper {
HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class);
HospitalDto toDto(Hospital hospital);
}
At this point, the adder method is used to add the converted subtype DTO objects to the collection fields of the parent type one by one.
public class CompanyMapperAdderPreferredImpl implements CompanyMapperAdderPreferred {
private final EmployeeMapper employeeMapper = Mappers.getMapper( EmployeeMapper.class );
@Override
public CompanyDTO map(Company company) {
if ( company == null ) {
return null;
}
CompanyDTO companyDTO = new CompanyDTO();
if ( company.getEmployees() != null ) {
for ( Employee employee : company.getEmployees() ) {
companyDTO.addEmployee( employeeMapper.map( employee ) );
}
}
return companyDTO;
}
}
If there is neither setter
nor adder
method in the target DTO, it will first get the subtype collection by getter
method, and then call the corresponding interface of the collection to add the subtype object.
One can see in the reference documentation the way of adding subtypes to a collection used when different types of DTO definitions (whether they contain setter methods or adder methods) with different mapping strategies are used.
Target collection realization type
MapStruct supports the collection interface as a target type for mapping methods.
In this case, some collection interface default implementations are used in the generated code. For example, in the above example, the default implementation of List
is ArrayList
.
Common interfaces and their corresponding default implementations are listed below:
Interface type | Implementation type |
---|---|
Collection | ArrayList |
List | ArrayList |
Map | HashMap |
SortedMap | TreeMap |
ConcurrentMap | ConcurrentHashMap |
You can find a list of all interfaces supported by MapStruct and the default implementation type for each interface in the reference documentation.
advanced operation
dependency injection
So far, we have been accessing the generated mapper through the getMapper()
method:
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
However, if you are using Spring, you can inject mappers like regular dependencies with a simple change to the mapper configuration.
Modify DoctorMapper
to support the Spring Framework:
@Mapper(componentModel = "spring")
public interface DoctorMapper {}
Adding (componentModel = "spring")
to the @Mapper
annotation is to tell MapStruct that when generating the mapper implementation class, we want it to support creation via Spring’s dependency injection. Now, there is no need to add the INSTANCE
field to the interface.
This time, DoctorMapperImpl
will be generated with the @Component
annotation:
@Component
public class DoctorMapperImpl implements DoctorMapper {}
As long as it is tagged with @Component
, Spring can treat it as a bean, and you can use it in other classes (e.g., controllers) via the @Autowire
annotation:
@Controller
public class DoctorController() {
@Autowired
private DoctorMapper doctorMapper;
}
If you don’t use Spring, MapStruct also supports Java CDI:
@Mapper(componentModel = "cdi")
public interface DoctorMapper {}
Adding default values
@Mapping
Two useful flags for annotations are the constant constant
and the default value defaultValue
. The constant value will always be used, regardless of the value of source
; if source
takes the value of null
, the default value will be used.
Make a change to DoctorMapper
and add a constant
and a defaultValue
:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(target = "id", constant = "-1")
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "Information Not Available")
DoctorDto toDto(Doctor doctor);
}
If specialty
is not available, we replace it with the "Information Not Available"
string, and in addition, we hardcode id
to -1
.
The generated code is as follows:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDto(Doctor doctor) {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
if (doctor.getSpecialty() != null) {
doctorDto.setSpecialization(doctor.getSpecialty());
}
else {
doctorDto.setSpecialization("Information Not Available");
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.setName(doctor.getName());
doctorDto.setId(-1);
return doctorDto;
}
}
As you can see, if doctor.getSpecialty()
returns a value of null
, then specialization
is set as our default message. In any case, a value is assigned to id
because it is a constant
.
Adding Expressions
MapStruct even allows Java expressions to be entered in the @Mapping
annotation. You can set defaultExpression
(which takes effect when source
is null
), or a expression
(which is a constant, permanent).
Two new attributes have been added to both the Doctor
and DoctorDto
classes, one for externalId
of type String
and the other for appointment
of type LocalDateTime
. The two classes are roughly as follows:
public class Doctor {
private int id;
private String name;
private String externalId;
private String specialty;
private LocalDateTime availability;
private List<Patient> patientList;
// getters and setters or builder
}
public class DoctorDto {
private int id;
private String name;
private String externalId;
private String specialization;
private LocalDateTime availability;
private List<PatientDto> patientDtoList;
// getters and setters or builder
}
Modify DoctorMapper
:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring", imports = {LocalDateTime.class, UUID.class})
public interface DoctorMapper {
@Mapping(target = "externalId", expression = "java(UUID.randomUUID().toString())")
@Mapping(source = "doctor.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())")
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDtoWithExpression(Doctor doctor);
}
As you can see, the value of externalId
is set to java(UUID.randomUUID().toString())
here, and if there is no availability
property in the source object, it will set availability
in the target object to a new LocalDateTime
object.
Since the expression is just a string, we have to specify the class to be used in the expression. But here the expression is not the final executed code, it is just a text value of a letter. Therefore, we have to add imports = {LocalDateTime.class, UUID.class}
to @Mapper
.
Adding custom methods
So far, the strategy we have been using is to add a “placeholder” method and expect MapStruct to implement it for us. We can actually add a custom default
method to the interface, or we can implement a map directly via the default
method. Then we can call that method directly from the instance without any problems.
To do this, we create a DoctorPatientSummary
class that contains summarized information about a Doctor
and its Patient
list:
public class DoctorPatientSummary {
private int doctorId;
private int patientCount;
private String doctorName;
private String specialization;
private String institute;
private List<Integer> patientIds;
// getters and setters or builder
}
Next, we add a default
method to DoctorMapper
that will convert the Doctor
and Education
objects into a DoctorPatientSummary
:
@Mapper
public interface DoctorMapper {
default DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
return DoctorPatientSummary.builder()
.doctorId(doctor.getId())
.doctorName(doctor.getName())
.patientCount(doctor.getPatientList().size())
.patientIds(doctor.getPatientList()
.stream()
.map(Patient::getId)
.collect(Collectors.toList()))
.institute(education.getInstitute())
.specialization(education.getDegreeName())
.build();
}
}
The Builder pattern is used here to create the DoctorPatientSummary
object.
After MapStruct generates the mapper implementation class, you can use this implementation method as you would access any other mapper method:
DoctorPatientSummary summary = doctorMapper.toDoctorPatientSummary(dotor, education);
Creating custom mappers
Previously we have been designing mapper functionality through interfaces, in fact we can also implement a mapper through a abstract
class with @Mapper
. MapStruct also creates an implementation for this class, similar to creating an interface implementation.
Let’s rewrite the previous example, this time modifying it to an abstract class:
@Mapper
public abstract class DoctorCustomMapper {
public DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
return DoctorPatientSummary.builder()
.doctorId(doctor.getId())
.doctorName(doctor.getName())
.patientCount(doctor.getPatientList().size())
.patientIds(doctor.getPatientList()
.stream()
.map(Patient::getId)
.collect(Collectors.toList()))
.institute(education.getInstitute())
.specialization(education.getDegreeName())
.build();
}
}
You can use this mapper in the same way. With fewer restrictions, using an abstract class gives us more control and options when creating custom implementations. Another benefit is the ability to add the @BeforeMapping
and @AfterMapping
methods.
@BeforeMapping 和 @AfterMapping
For further control and customization, we can define the @BeforeMapping
and @AfterMapping
methods. Obviously, these two methods are executed before and after each mapping. That is, in the final implementation code, these two methods will be added and executed before and after the two objects are actually mapped.
Two methods can be added to DoctorCustomMapper
:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public abstract class DoctorCustomMapper {
@BeforeMapping
protected void validate(Doctor doctor) {
if(doctor.getPatientList() == null){
doctor.setPatientList(new ArrayList<>());
}
}
@AfterMapping
protected void updateResult(@MappingTarget DoctorDto doctorDto) {
doctorDto.setName(doctorDto.getName().toUpperCase());
doctorDto.setDegree(doctorDto.getDegree().toUpperCase());
doctorDto.setSpecialization(doctorDto.getSpecialization().toUpperCase());
}
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
public abstract DoctorDto toDoctorDto(Doctor doctor);
}
Generate a mapper implementation class based on this abstract class:
@Component
public class DoctorCustomMapperImpl extends DoctorCustomMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDoctorDto(Doctor doctor) {
validate(doctor);
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
updateResult(doctorDto);
return doctorDto;
}
}
As you can see, the validate()
method will be executed before the instantiation of the DoctorDto
object, while the updateResult()
method will be executed after the end of the mapping.
Mapping Exception Handling
Exception handling is unavoidable , the application at any time will generate an exception state . MapStruct provides support for exception handling , you can simplify the developer’s work .
Consider a scenario where we want to validate the data from Doctor
before mapping Doctor
to DoctorDto
. We create a new standalone Validator
class to do the validation:
public class Validator {
public int validateId(int id) throws ValidationException {
if(id == -1){
throw new ValidationException("Invalid value in ID");
}
return id;
}
}
Let’s modify DoctorMapper
to use the Validator
class without specifying an implementation. As before, add the class to the list of classes used by @Mapper
. All we need to do is tell MapStruct that our toDto()
will throw throws ValidationException
:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor) throws ValidationException;
}
The final generated mapper code is as follows:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Autowired
private Validator validator;
@Override
public DoctorDto toDto(Doctor doctor) throws ValidationException {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setId(validator.validateId(doctor.getId()));
doctorDto.setName(doctor.getName());
doctorDto.setExternalId(doctor.getExternalId());
doctorDto.setAvailability(doctor.getAvailability());
return doctorDto;
}
}
MapStruct automatically sets id
of doctorDto
to the method return value of the Validator
instance. It also adds a throws clause to that method signature.
Note that if the type of a pair of attributes before and after the mapping is the same as the in/out parameter type of the method in Validator
, then the method in Validator
will be called when the field is mapped, so please use this method with caution.
mapping configuration
MapStruct provides some very useful configurations for writing mapper methods. Most of the time, if we have already defined a mapping method between two types, when we want to add another mapping method between the same types, we tend to just copy the mapping configuration of the existing method.
We don’t actually have to copy these annotations manually, just a simple configuration is needed to create an identical/similar mapping method.
Inheritance Configuration
Let’s review ” Update Existing Instance”, in that scenario we created a mapper that updates the property values of an existing Doctor object based on the properties of the DoctorDto object:
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
Suppose we have another mapper that converts DoctorDto
to Doctor
:
@Mapper(uses = {PatientMapper.class, Validator.class})
public interface DoctorMapper {
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
Doctor toModel(DoctorDto doctorDto);
}
These two mapper methods use the same annotation configuration, source
and target
are the same. In fact, we can use the @InheritConfiguration
annotation, thus avoiding duplicate configurations for these two mapper methods.
If the @InheritConfiguration
annotation is added to a method, MapStruct searches other configured methods for an annotation configuration that can be used for the current method. Generally, this annotation is used for the update
method after the mapping
method, as shown below:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctorDto.specialization", target = "specialty")
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
Doctor toModel(DoctorDto doctorDto);
@InheritConfiguration
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
Inheritance Reverse Configuration
There is another similar scenario of writing mapping functions to convert a Model to a DTO and a DTO to a Model. as shown in the code below, we have to add the same comment on both functions.
@Mapper(componentModel = "spring")
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
PatientDto toDto(Patient patient);
}
The configuration of the two methods will not be identical; in fact, they should be opposite. Converting Model to DTO and DTO to Model – the fields are the same before and after the mapping, but the source attribute field is the opposite of the target attribute field.
We can use the @InheritInverseConfiguration
annotation on the second method to avoid writing the mapping configuration twice:
@Mapper(componentModel = "spring")
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
@InheritInverseConfiguration
PatientDto toDto(Patient patient);
}
The code generated by these two Mappers is the same.
In this article, we explore MapStruct – a library for creating mapper classes. From basic mappings to custom methods and custom mappers, in addition, we cover some of the advanced manipulation options provided by MapStruct, including dependency injection, data type mapping, enumeration mapping, and expression usage.
MapStruct provides a powerful integrated plug-in that reduces the developer’s effort in writing template code and makes the process of creating mappers quick and easy.
To explore more detailed ways of using MapStruct, you can refer to the official MapStruct Reference Guide.