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 and Integer , float and Float , long and Long , boolean and Boolean , etc.

  • Between any base type and any wrapper class. For example, int and long , byte and Integer , etc.

  • All basic types and wrapper classes with String . For example, boolean and String , Integer and String , float and String , 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) and String .
  •  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 typeImplementation type
CollectionArrayList
ListArrayList
MapHashMap
SortedMapTreeMap
ConcurrentMapConcurrentHashMap


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.

By lzz

Leave a Reply

Your email address will not be published. Required fields are marked *