MapStruct Mapper

About

MapStruct is a code generator that simplifies the implementation of mappings between Java bean types based on a convention-over-configuration approach. It is a tool designed to help developers map data from one Java object to another. It is a popular choice for mapping objects, especially in large-scale enterprise applications, due to its performance and ease of use.

Refer to documentation for more details: https://mapstruct.org/documentation/1.5/reference/html/

What is "Convention-Over-Configuration"?

"Convention-over-configuration" is a software design principle used to reduce the number of decisions developers need to make, without sacrificing flexibility. In simpler terms, it means that the framework will assume reasonable default behavior unless the developer specifies otherwise. This approach minimizes the need for extensive configuration by relying on common conventions.

How Does MapStruct Use Convention-Over-Configuration?

MapStruct uses this principle to simplify object mappings by following these conventions

  • Property Name Matching:

    • By default, MapStruct maps properties in source and target objects that have the same name and compatible types.

    • For example, if we have a UserDTO class with a name field and a UserEntity class with a name field, MapStruct will automatically map UserDTO.name to UserEntity.name.

  • Type Matching:

    • MapStruct can automatically convert between types that have a clear conversion path (e.g., String to int, Date to String).

    • This reduces the need for explicit type conversion configuration.

  • Default Method Generation:

    • MapStruct generates implementation code for the mapping methods based on the interface definitions provided by the developer.

    • For instance, if we define a method UserDTO toUserDTO(UserEntity entity); in a mapper interface, MapStruct will generate the implementation for this method, mapping each field based on conventions.

Maven POM Dependency and Plugin

Include the required dependencies in pom.xml file.

<!-- This dependency includes the core MapStruct library which provides 
the API and main functionality for object mapping. -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>
<!-- This dependency includes the MapStruct annotation processor which 
generates the implementation of the mapper interfaces at compile time. -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.5.Final</version>
</dependency>

It comprises the following artifacts:

  • org.mapstruct:mapstruct: contains the required annotations such as @Mapping

  • org.mapstruct:mapstruct-processor: contains the annotation processor which generates mapper implementations

<!-- The maven-compiler-plugin is a Maven plugin used to compile Java source files. 
In the context of using MapStruct, it is configured to ensure that the MapStruct annotation 
processor is correctly set up during the compilation phase. -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${maven-compiler-plugin.version}</version>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>${mapstruct.version}</version>
            </path>
        </annotationProcessorPaths>
        <useIncrementalCompilation>false</useIncrementalCompilation>
        <showWarnings>true</showWarnings>
        <compilerArgs>
            <arg>-Amapstruct.suppressGeneratorTimestamp=true</arg>
            <arg>-Amapstruct.suppressGeneratorVersionInfoComment=true</arg>
            <arg>-Amapstruct.verbose=true</arg>
        </compilerArgs>
    </configuration>
</plugin>

MapStruct processor options -

Core Features

MapStruct provides a set of core features that allow to map properties between different objects seamlessly. These include:

  • Basic Type Mapping: MapStruct automatically maps properties with the same name and compatible types.

  • Handling Null Values: By default, MapStruct maps null values, but we can customize this behavior.

  • Customizing Mappings with @Mapping: This annotation allows to define how individual fields are mapped.

Java Code

Basic Mapping

The @Mapper annotation causes the MapStruct code generator to create an implementation of the UserMapper interface during build-time.

  • When a property has the same name as its target entity counterpart, it will be mapped implicitly.

  • When a property has a different name in the target entity, its name can be specified via the @Mapping annotation.

  • The DTO or the objects being mapped should have getter setter to access fields.

  • @BeanMapping with the ignoreByDefault attribute in MapStruct allows us to specify that all properties should be ignored by default, and we can then explicitly specify which properties to include in the mapping. This can be useful for partial updates or when you want to map only a subset of the properties.

  • In some cases, it can be required to manually implement a specific mapping from one type to another which can’t be generated by MapStruct. Use the Default method for such case.

  • In some cases, we may need mappings which don’t create a new instance of the target type but instead update an existing instance of that type. We can achieve this by adding a parameter for the target object and marking this parameter with @MappingTarget. There may be only one parameter marked as mapping target.

  • MapStruct also supports mappings of public fields that have no getters/setters. MapStruct will use the fields as read/write accessor if it cannot find suitable getter/setter methods for the property. A field is considered as a read accessor if it is public or public final. If a field is static it is not considered as a read accessor. A field is considered as a write accessor only if it is public. If a field is final and/or static it is not considered as a write accessor.

package mapper;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(componentModel = "spring")
public interface UserMapper {

    // UserId will be mapped implicitly since field name from both the object is same
    @Mapping(target = "userName", source = "name")
    @Mapping(target = "userEmail", source = "email")
    @Mapping(target = "addressDTO.pincode", source = "pincode")
    UserDTO mapToUserDTO(User user);
    
    // All properties need to map explicitly
    @BeanMapping(ignoreByDefault = true)
    AddressDTO mapToAddressDTO(Address address);
    
    // Implement custom method if it has complex mapping logic which cannot be handled by mapstruct
    default PersonDto personToPersonDto(Person person) {
        //hand-written mapping logic
    }
    
    // Mapping methods with several source parameters
    @Mapping(target = "description", source = "person.description")
    @Mapping(target = "houseNumber", source = "address.houseNo")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
    
    // Mapping nested bean properties to current target. "." as target
    // Generated code will map every property from CustomerDto.record and Customer.account
    // to Customer directly, without manually naming. 
    @Mapping( target = "name", source = "record.name" )
    @Mapping( target = ".", source = "record" )
    @Mapping( target = ".", source = "account" )
    Customer customerDtoToCustomer(CustomerDto customerDto);
    
    // Updating existing bean instances
    // In some cases, we need to update an existing instance of a type rather a new instance
    void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
    
    // We can return the target type as well
    Car updateAndReturnCarFromDto(CarDto carDto, @MappingTarget Car car);
    
    // Here, say CustomerDto has only public fields but no getter setter. Whereas Customer has private fields with getter setter
    // Mapstruct allows mapping even if no getter setter provided it satisfies accessor conditons
    @Mapping(target = "name", source = "customerName")
    Customer toCustomer(CustomerDto customerDto);
    
    // Annotation @InheritInverseConfiguration indicates that a method shall inherit the inverse configuration of the corresponding reverse method.
    @InheritInverseConfiguration
    CustomerDto fromCustomer(Customer customer);
}

Using builders

MapStruct also supports mapping of immutable types via builders. When performing a mapping MapStruct checks if there is a builder for the type being mapped. This is done via the BuilderProvider SPI. If a Builder exists for a certain type, then that builder will be used for the mappings.

// Builder Pattern for Person class
public class Person {

    private final String name;

    protected Person(Person.Builder builder) {
        this.name = builder.name;
    }
    public static Person.Builder builder() {
        return new Person.Builder();
    }

    public static class Builder {

        private String name;

        public Builder name(String name) {
            this.name = name;
            return this;
        }
        public Person create() {
            return new Person( this );
        }
    }
}

// Mapstruct mapper
@Mapper(componentModel = "spring")
public interface PersonMapper {
    Person map(PersonDto dto);
}

Using Constructors

MapStruct supports using constructors for mapping target types. When doing a mapping MapStruct checks if there is a builder for the type being mapped. If there is no builder, then MapStruct looks for a single accessible constructor.

  • If a constructor is annotated with an annotation named @Default it will be used.

  • If a single public constructor exists then it will be used to construct the object, and the other non public constructors will be ignored.

  • If a parameterless constructor exists then it will be used to construct the object, and the other constructors will be ignored.

  • If there are multiple eligible constructors then there will be a compilation error due to ambiguous constructors.

public class Vehicle {
    protected Vehicle() { }
    // MapStruct will use this constructor, because it is a single public constructor
    public Vehicle(String color) { }
}

public class Car {
    // MapStruct will use this constructor, because it is a parameterless empty constructor
    public Car() { }
    public Car(String make, String color) { }
}

public class Truck {
    public Truck() { }
    // MapStruct will use this constructor, because it is annotated with @Default
    @Default
    public Truck(String make, String color) { }
}

public class Van {
    // There will be a compilation error when using this class because MapStruct cannot pick a constructor
    public Van(String make) { }
    public Van(String make, String color) { }
}

// Mapper
@Mapper(componentModel = "spring")
public interface PersonMapper {
    Person map(PersonDto dto);
}

Composition Mapping

MapStruct supports the use of meta annotations like @Retention. This allows @Mapping to be used on other (user defined) annotations for re-use purposes.

For example below. The @ToTransactionHeader assumes both target beans TransactionEntity and PaymentEntity have properties: "id", "creationDate" and "name"

@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", ignore = true)
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
@Mapping(target = "name", source = "groupName")
public @interface ToTransactionHeader { }

@Mapper
public interface TransactionMapper {

    TransactionMapper INSTANCE = Mappers.getMapper( TransactionMapper.class );

    @ToTransactionHeader
    @Mapping( target = "type", source = "type")
    TransactionEntity map(TransactionDto source);

    @ToTransactionHeader
    @Mapping( target = "accountId", source = "accountId")
    PaymentEntity map(PaymentDto source);
}

Last updated