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/
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 -
mapstruct. suppressGeneratorTimestamp
If set to true
, the creation of a time stamp in the @Generated
annotation in the generated mapper classes is suppressed.
false
mapstruct.verbose
If set to true
, MapStruct in which MapStruct logs its major decisions. Note, at the moment of writing in Maven, also showWarnings
needs to be added due to a problem in the maven-compiler-plugin configuration.
false
mapstruct. suppressGeneratorVersionInfoComment
If set to true
, the creation of the comment
attribute in the @Generated
annotation in the generated mapper classes is suppressed. The comment contains information about the version of MapStruct and about the compiler used for the annotation processing.
false
mapstruct.defaultComponentModel
The name of the component model based on which mappers should be generated.
Supported values are:
default
: the mapper uses no component model, instances are typically retrieved viaMappers#getMapper(Class)
cdi
: the generated mapper is an application-scoped (from javax.enterprise.context or jakarta.enterprise.context, depending on which one is available with javax.inject having priority) CDI bean and can be retrieved via@Inject
spring
: the generated mapper is a singleton-scoped Spring bean and can be retrieved via@Autowired
or lombok annotation like@RequiredArgsConstructor
jsr330
: the generated mapper is annotated with {@code @Named} and can be retrieved via@Inject
(from javax.inject or jakarta.inject, depending which one is available with javax.inject having priority), e.g. using Springjakarta
: the generated mapper is annotated with {@code @Named} and can be retrieved via@Inject
(from jakarta.inject), e.g. using Springjakarta-cdi
: the generated mapper is an application-scoped (from jakarta.enterprise.context) CDI bean and can be retrieved via@Inject
If a component model is given for a specific mapper via @Mapper#componentModel()
, the value from the annotation takes precedence.
default
mapstruct.defaultInjectionStrategy
The type of the injection in mapper via parameter uses
. This is only used on annotated based component models such as CDI, Spring and JSR 330.
Supported values are:
field
: dependencies will be injected in fieldsconstructor
: will be generated constructor. Dependencies will be injected via constructor.
When CDI componentModel
a default constructor will also be generated. If a injection strategy is given for a specific mapper via @Mapper#injectionStrategy()
, the value from the annotation takes precedence over the option.
field
mapstruct.unmappedTargetPolicy
The default reporting policy to be applied in case an attribute of the target object of a mapping method is not populated with a source value.
Supported values are:
ERROR
: any unmapped target property will cause the mapping code generation to failWARN
: any unmapped target property will cause a warning at build timeIGNORE
: unmapped target properties are ignored
If a policy is given for a specific mapper via @Mapper#unmappedTargetPolicy()
, the value from the annotation takes precedence. If a policy is given for a specific bean mapping via @BeanMapping#unmappedTargetPolicy()
, it takes precedence over both @Mapper#unmappedTargetPolicy()
and the option.
WARN
mapstruct.unmappedSourcePolicy
The default reporting policy to be applied in case an attribute of the source object of a mapping method is not populated with a target value.
Supported values are:
ERROR
: any unmapped source property will cause the mapping code generation to failWARN
: any unmapped source property will cause a warning at build timeIGNORE
: unmapped source properties are ignored
If a policy is given for a specific mapper via @Mapper#unmappedSourcePolicy()
, the value from the annotation takes precedence. If a policy is given for a specific bean mapping via @BeanMapping#ignoreUnmappedSourceProperties()
, it takes precedence over both @Mapper#unmappedSourcePolicy()
and the option.
WARN
mapstruct. disableBuilders
If set to true
, then MapStruct will not use builder patterns when doing the mapping. This is equivalent to doing @Mapper( builder = @Builder( disableBuilder = true ) )
for all of our mappers.
false
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.
Data type conversions
It is possible to have mapped attribute with the same type or different in the source and target objects. We need to understand how MapStruct deals with such data type conversions.
Implicit type conversions
Mapstruct applies the following conversion automatically.
Between all Java primitive data types and their corresponding wrapper types, e.g. between
int
andInteger
,boolean
andBoolean
etc. When converting a wrapper type into the corresponding primitive type anull
check will be performed.Between all Java primitive number types and the wrapper types, e.g. between
int
andlong
orbyte
andInteger
Between all Java primitive types (including their wrappers) and
String
, e.g. betweenint
andString
orBoolean
andString
. A format string as understood byjava.text.DecimalFormat
can be specified.
@Mapper
public interface CarMapper {
@Mapping(source = "price", numberFormat = "$#.00")
CarDto carToCarDto(Car car);
@IterableMapping(numberFormat = "$#.00")
List<String> prices(List<Integer> prices);
}
Between
enum
types andString
.Between big number types (
java.math.BigInteger
,java.math.BigDecimal
) and Java primitive types (including their wrappers) as well as String. A format stringjava.text.DecimalFormat
can be specified.
@Mapper
public interface EventMapper {
@Mapping(source = "fee", numberFormat = "#.##E0")
EventDto eventToEventDto(Event event);
}
Between
JAXBElement<T>
andT
,List<JAXBElement<T>>
andList<T>
Between
java.util.Calendar
/java.util.Date
and JAXB’sXMLGregorianCalendar
Between
java.util.Date
/XMLGregorianCalendar
andString
. A format string as understood byjava.text.SimpleDateFormat
can be specified via thedateFormat
option
@Mapper
public interface EventMapper {
@Mapping(source = "paymenDate", dateFormat = "dd.MM.yyyy")
EventDto eventToEventDto(Event event);
@IterableMapping(dateFormat = "dd.MM.yyyy")
List<String> stringListToDateList(List<Date> dates);
}
Between Jodas
org.joda.time.DateTime
,org.joda.time.LocalDateTime
,org.joda.time.LocalDate
,org.joda.time.LocalTime
andString
. A format string as understood byjava.text.SimpleDateFormat
can be specified via thedateFormat
option (see above).Between Jodas
org.joda.time.DateTime
andjavax.xml.datatype.XMLGregorianCalendar
,java.util.Calendar
.Between Jodas
org.joda.time.LocalDateTime
,org.joda.time.LocalDate
andjavax.xml.datatype.XMLGregorianCalendar
,java.util.Date
.Between
java.time.LocalDate
,java.time.LocalDateTime
andjavax.xml.datatype.XMLGregorianCalendar
.Between
java.time.ZonedDateTime
,java.time.LocalDateTime
,java.time.LocalDate
,java.time.LocalTime
from Java 8 Date-Time package andString
. A format string as understood byjava.text.SimpleDateFormat
can be specified via thedateFormat
option (see above).Between
java.time.Instant
,java.time.Duration
,java.time.Period
from Java 8 Date-Time package andString
using theparse
method in each class to map fromString
and usingtoString
to map intoString
.Between
java.time.ZonedDateTime
from Java 8 Date-Time package andjava.util.Date
where, when mapping aZonedDateTime
from a givenDate
, the system default timezone is used.Between
java.time.LocalDateTime
from Java 8 Date-Time package andjava.util.Date
where timezone UTC is used as the timezone.Between
java.time.LocalDate
from Java 8 Date-Time package andjava.util.Date
/java.sql.Date
where timezone UTC is used as the timezone.Between
java.time.Instant
from Java 8 Date-Time package andjava.util.Date
.Between
java.time.ZonedDateTime
from Java 8 Date-Time package andjava.util.Calendar
.Between
java.sql.Date
andjava.util.Date
Between
java.sql.Time
andjava.util.Date
Between
java.sql.Timestamp
andjava.util.Date
When converting from a
String
, omittingMapping#dateFormat
, it leads to usage of the default pattern and date format symbols for the default locale. An exception to this rule isXmlGregorianCalendar
which results in parsing theString
according to XML Schema.Between
java.util.Currency
andString
.When converting from a
String
, the value needs to be a valid ISO-4217 alphabetic code otherwise anIllegalArgumentException
is thrown.
Between
java.util.UUID
andString
.When converting from a
String
, the value needs to be a valid UUID otherwise anIllegalArgumentException
is thrown.
Between
String
andStringBuilder
Between
java.net.URL
andString
.When converting from a
String
, the value needs to be a valid URL otherwise aMalformedURLException
is thrown.
Mapping nested object references
Suppose, Event has reference to Address object.
@Mapper
public interface EventMapper {
EventDto eventToEventDto(Event event);
AddressDto addressToAddressDto(Address address);
}
Controlling nested bean mappings
MapStruct will generate a method based on the name of the source and target property. In many occasions these names do not match. The ‘.’ notation in an @Mapping
source or target type can be used to control how properties should be mapped when names do not match.
@Mapper
public interface FishTankMapper {
@Mapping(target = "fish.kind", source = "fish.type")
@Mapping(target = "fish.name", ignore = true)
@Mapping(target = "ornament", source = "interior.ornament")
@Mapping(target = "material.materialType", source = "material")
@Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")
FishTankDto map( FishTank source );
}
Invoking custom mapping method
Sometimes, some fields require custom logic. For example, MapStruct will take the entire parameter source
and generate code to call the custom method mapVolume
in order to map the FishTank
object to the target property volume
.
public class FishTank {
Fish fish;
String material;
Quality quality;
int length;
int width;
int height;
}
public class FishTankWithVolumeDto {
FishDto fish;
MaterialDto material;
QualityDto quality;
VolumeDto volume;
}
public class VolumeDto {
int volume;
String description;
}
@Mapper
public abstract class FishTankMapperWithVolume {
@Mapping(target = "fish.kind", source = "source.fish.type")
@Mapping(target = "material.materialType", source = "source.material")
@Mapping(target = "quality.document", source = "source.quality.report")
@Mapping(target = "volume", source = "source")
abstract FishTankWithVolumeDto map(FishTank source);
VolumeDto mapVolume(FishTank source) {
int volume = source.length * source.width * source.height;
String desc = volume < 100 ? "Small" : "Large";
return new VolumeDto(volume, desc);
}
}
Invoking other mappers
MapStruct can also invoke mapping methods defined in other classes, be it mappers generated by MapStruct or hand-written mapping methods. For eg, when generating code for the implementation of the carToCarDto()
method, MapStruct will look for a method which maps a Date
object into a String, find it on the DateMapper
class and generate an invocation of asString()
for mapping the manufacturingDate
attribute.
// Manually implemented class
public class DateMapper {
public String asString(Date date) {
return date != null ? new SimpleDateFormat( "yyyy-MM-dd" )
.format( date ) : null;
}
public Date asDate(String date) {
try {
return date != null ? new SimpleDateFormat( "yyyy-MM-dd" )
.parse( date ) : null;
}
catch ( ParseException e ) {
throw new RuntimeException( e );
}
}
}
// Using the DateMapper in a mapper
@Mapper(uses=DateMapper.class)
public interface CarMapper {
CarDto carToCarDto(Car car);
}
Passing context or state objects to custom methods
Additional context or state information can be passed through generated mapping methods to custom methods with @Context
parameters. Such parameters are passed to other mapping methods, @ObjectFactory
methods or @BeforeMapping
/ @AfterMapping
methods when applicable and can thus be used in custom code.
public abstract CarDto toCar(Car car, @Context Locale translationLocale);
protected OwnerManualDto translateOwnerManual(OwnerManual ownerManual, @Context Locale locale) {
// manually implemented logic to translate the OwnerManual with the given Locale
}
//GENERATED CODE
public CarDto toCar(Car car, Locale translationLocale) {
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
carDto.setOwnerManual( translateOwnerManual( car.getOwnerManual(), translationLocale );
// more generated mapping code
return carDto;
}
Mapping method resolution
When mapping a property from one type to another, MapStruct looks for the most specific method which maps the source type into the target type. The method may either be declared on the same mapper interface or on another mapper which is registered via @Mapper#uses()
Combining qualifiers @Named with defaults
Default value will be used if the returned value from the @Named method is null.
@Mapper
public interface MovieMapper {
@Mapping( target = "category", qualifiedByName = "CategoryToString", defaultValue = "DEFAULT" )
GermanRelease toGerman( OriginalRelease movies );
@Named("CategoryToString")
default String defaultValueForQualifier(Category cat) {
// some mapping logic
}
}
Basic Mapping
The @Mapper
annotation causes the MapStruct code generator to create an implementation of the UserMapper
interface during build-time. MapStruct will generate a method based on the name of the source and target property.
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);
}
Default values and constants
Default values and constants are specified as String values. If the source is null, default value will be used.
@Mapper(uses = StringListMapper.class)
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001")
@Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
@Mapping(target = "stringListConstants", constant = "jack-jill-tom")
Target sourceToTarget(Source s);
}
Expressions
As per documentation, MapStruct will not validate the java expression at generation-time, but errors will show up in the generated classes during compilation. Fully qualified package name is specified because MapStruct does not take care of the import of the TimeAndFormat
class (unless it’s used otherwise explicitly in the SourceTargetMapper
). This can be resolved by defining imports
on the @Mapper
annotation.
@Mapper
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target = "timeAndFormat",
expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
}
imports org.sample.TimeAndFormat;
@Mapper( imports = TimeAndFormat.class )
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target = "timeAndFormat",
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
}
Default Expressions
Default expressions are a combination of default values and expressions. They will only be used when the source attribute is null
.
imports java.util.UUID;
@Mapper( imports = UUID.class )
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
Target sourceToTarget(Source s);
}
Subclass Mapping
It is used when both input and result types have an inheritance relation. Suppose an Apple
and a Banana
, which are both specializations of Fruit
.
@Mapper
public interface FruitMapper {
@SubclassMapping( source = AppleDto.class, target = Apple.class )
@SubclassMapping( source = BananaDto.class, target = Banana.class )
Fruit map( FruitDto source );
}
If we would just use a normal mapping both the AppleDto
and the BananaDto
would be made into a Fruit
object, instead of an Apple
and a Banana
object.
Determining the result type
When result types have an inheritance relation, selecting either mapping method (@Mapping
) or a factory method (@BeanMapping
) can become ambiguous. Suppose an Apple and a Banana, which are both specializations of Fruit.
public class FruitFactory {
public Apple createApple() {
return new Apple( "Apple" );
}
public Banana createBanana() {
return new Banana( "Banana" );
}
}
@Mapper( uses = FruitFactory.class )
public interface FruitMapper {
@BeanMapping( resultType = Apple.class )
Fruit map( FruitDto source );
}
Controlling mapping result for 'null' arguments
As per documentation, when the source argument of the mapping method equals null
then by default null
will be returned.
However, by specifying nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT
on @BeanMapping
, @IterableMapping
, @MapMapping
, or globally on @Mapper
or @MapperConfig
, the mapping result can be altered to return empty default values. This means for:
Bean mappings: an 'empty' target bean will be returned, with the exception of constants and expressions, they will be populated when present.
Iterables / Arrays: an empty iterable will be returned.
Maps: an empty map will be returned.
Source presence checking and Conditional Mapping
Some frameworks generate bean properties that have a source presence checker. Often this is in the form of a method hasXYZ
, XYZ
being a property on the source bean in a bean mapping method. MapStruct will call this hasXYZ
instead of performing a null
check when it finds such hasXYZ
method.
Conditional Mapping is a type of Source presence checking. The difference is that it allows users to write custom condition methods that will be invoked to check if a property needs to be mapped or not.
A custom condition method is a method that is annotated with org.mapstruct.Condition
and returns boolean
.
e.g. if you only want to map a String property when it is not `null, and it is not empty then you can do something like:
@Mapper
public interface CarMapper {
CarDto carToCarDto(Car car);
@Condition
default boolean isNotEmpty(String value) {
return value != null && !value.isEmpty();
}
}
When using this in combination with an update mapping method it will replace the null-check
there, for example:
@Mapper
public interface CarMapper {
CarDto carToCarDto(Car car, @MappingTarget CarDto carDto);
@Condition
default boolean isNotEmpty(String value) {
return value != null && !value.isEmpty();
}
}
Exceptions
Calling applications may require handling of exceptions when calling a mapping method. These exceptions could be thrown by hand-written logic and by the generated built-in mapping methods or type-conversions of MapStruct.
@Mapper(uses = HandWritten.class)
public interface CarMapper {
CarDto carToCarDto(Car car) throws GearException;
}
public class HandWritten {
private static final String[] GEAR = {"ONE", "TWO", "THREE", "OVERDRIVE", "REVERSE"};
public String toGear(Integer gear) throws GearException, FatalException {
if ( gear == null ) {
throw new FatalException("null is not a valid gear");
}
if ( gear < 0 && gear > GEAR.length ) {
throw new GearException("invalid gear");
}
return GEAR[gear];
}
}
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);
}
Mapping Map
to Bean
Map
to BeanWe want to map Map<String, ???>
into a specific bean. When a raw map or a map that does not have a String as a key is used, then a warning will be generated.
public class Customer {
private Long id;
private String name;
//getters and setter omitted for brevity
}
@Mapper
public interface CustomerMapper {
// Here, source act as key. Mapstruct will try to use map.containsKey( "customerName" ) to check and then ex
// map.get( "id" ) to get the value and assign to target
@Mapping(target = "name", source = "customerName")
Customer toCustomer(Map<String, String> map);
}
Retrieving a mapper in another class to use its methods
Without using DI framework
CarMapper mapper = Mappers.getMapper( CarMapper.class );
But with this, we need to repeatedly instantiating new instances if we need to use it in several classes. To fix this, a mapper interface should define a member called INSTANCE
which holds a single instance of the mapper type.
// Declaring an instance of a mapper (interface)
@Mapper(componentModel = ComponentModel.SPRING)
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
CarDto carToCarDto(Car car);
}
// Declaring an instance of a mapper (abstract class)
@Mapper
public abstract class CarMapper {
public static final CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
CarDto carToCarDto(Car car);
}
// Usage in other classes
CarDto dto = CarMapper.INSTANCE.carToCarDto( car );
Using DI framework
When using Spring Framework, it is recommended to obtain mapper objects via dependency injection and not via the Mappers
class as described above.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface EventMapper {
...
}
// Usage
@RequiredArgsConstructor
@Component
public class SomeEventProcessor {
...
private final EventMapper eventMapper;
...
..
}
Injection strategy
When using dependency injection, we can choose between field and constructor injection. This can be done by providing injection strategy via @Mapper
or @MapperConfig
annotation. Constructor injection is recommended to simplify testing. As per documentation, for abstract classes or decorators setter injection should be used.
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface CarMapper {
CarDto carToCarDto(Car car);
}
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);
}
Mapping collections
As per documentation, the mapping of collection types (List
, Set
etc.) is done in the same way as mapping bean types, i.e. by defining mapping methods with the required source and target types in a mapper interface
@Mapper
public interface CarMapper {
Set<String> integerSetToStringSet(Set<Integer> integers);
List<CarDto> carsToCarDtos(List<Car> cars);
CarDto carToCarDto(Car car);
}
Mapping maps
public interface SourceTargetMapper {
@MapMapping(valueDateFormat = "dd.MM.yyyy")
Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}
Implementation types used for collection mappings
As per documentation, when an iterable or map mapping method declares an interface type as return type, one of its implementation types will be instantiated in the generated code.
Iterable
ArrayList
Collection
ArrayList
List
ArrayList
Set
LinkedHashSet
SortedSet
TreeSet
NavigableSet
TreeSet
Map
LinkedHashMap
SortedMap
TreeMap
NavigableMap
TreeMap
ConcurrentMap
ConcurrentHashMap
ConcurrentNavigableMap
ConcurrentSkipListMap
Mapping Streams
As per documentation, mapping of java.util.Stream
is done in a similar way as the mapping of collection types, i.e. by defining mapping methods with the required source and target types in a mapper interface.
@Mapper
public interface CarMapper {
Set<String> integerStreamToStringSet(Stream<Integer> integers);
List<CarDto> carsToCarDtos(Stream<Car> cars);
CarDto carToCarDto(Car car);
}
Mapping Values
Mapping enum to enum types
@Mapper
public interface OrderMapper {
OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class );
@ValueMappings({
@ValueMapping(target = "SPECIAL", source = "EXTRA"),
@ValueMapping(target = "DEFAULT", source = "STANDARD"),
@ValueMapping(target = "DEFAULT", source = "NORMAL")
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}
@Mapper
public interface SpecialOrderMapper {
SpecialOrderMapper INSTANCE = Mappers.getMapper( SpecialOrderMapper.class );
// MapStruct would have refrained from mapping the RETAIL and B2B when <ANY_UNMAPPED> was used instead of <ANY_REMAINING>
@ValueMappings({
@ValueMapping( source = MappingConstants.NULL, target = "DEFAULT" ),
@ValueMapping( source = "STANDARD", target = MappingConstants.NULL ),
@ValueMapping( source = MappingConstants.ANY_REMAINING, target = "SPECIAL" )
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}
@Mapper
public interface SpecialOrderMapper {
SpecialOrderMapper INSTANCE = Mappers.getMapper( SpecialOrderMapper.class );
@ValueMappings({
@ValueMapping( source = "STANDARD", target = "DEFAULT" ),
@ValueMapping( source = "C2C", target = MappingConstants.THROW_EXCEPTION )
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}
Mapping enum-to-String or String-to-enum
MapStruct supports enum to a String mapping on the similar lines.
Custom name transformation
As per documentation, when no @ValueMapping
(s) are defined then each constant from the source enum is mapped to a constant with the same name in the target enum type. However, there are cases where the source enum needs to be transformed before doing the mapping. E.g. a suffix needs to be applied to map from the source into the target enum.
MapStruct provides the following enum name transformation strategies:
suffix - Applies a suffix on the source enum
stripSuffix - Strips a suffix from the source enum
prefix - Applies a prefix on the source enum
stripPrefix - Strips a prefix from the source enum
case - Applies case transformation to the source enum. Supported case transformations are:
upper - Performs upper case transformation to the source enum
lower - Performs lower case transformation to the source enum
capital - Performs capitalisation of the first character of every word in the source enum and everything else to lowercase. A word is split by "_"
@Mapper
public interface CheeseMapper {
CheeseMapper INSTANCE = Mappers.getMapper( CheeseMapper.class );
@EnumMapping(nameTransformationStrategy = "suffix", configuration = "_TYPE")
CheeseTypeSuffixed map(CheeseType cheese);
@InheritInverseConfiguration
CheeseType map(CheeseTypeSuffix cheese);
}
Object factories
By default, the generated code for mapping one bean type into another or updating a bean will call the default constructor to instantiate the target type. Alternatively, Mapstruct supports custom object factories which will be invoked to obtain instances of the target type.
public class DtoFactory {
public CarDto createCarDto() {
return // ... custom factory logic
}
}
public class EntityFactory {
public <T extends BaseEntity> T createEntity(@TargetType Class<T> entityClass) {
return // ... custom factory logic
}
}
@Mapper(uses= { DtoFactory.class, EntityFactory.class } )
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
CarDto carToCarDto(Car car);
Car carDtoToCar(CarDto carDto);
}
@Mapper(uses = { DtoFactory.class, EntityFactory.class, CarMapper.class } )
public interface OwnerMapper {
OwnerMapper INSTANCE = Mappers.getMapper( OwnerMapper.class );
void updateOwnerDto(Owner owner, @MappingTarget OwnerDto ownerDto);
void updateOwner(OwnerDto ownerDto, @MappingTarget Owner owner);
}
Reusing mapping configurations
Mapping configuration inheritance
Method-level configuration annotations such as @Mapping
, @BeanMapping
, @IterableMapping
, etc., can be inheritedfrom one mapping method to a similar method using the annotation @InheritConfiguration
@Mapper
public interface CarMapper {
@Mapping(target = "numberOfSeats", source = "seatCount")
Car carDtoToCar(CarDto car);
@InheritConfiguration
void carDtoIntoCar(CarDto carDto, @MappingTarget Car car);
}
Inverse mappings
In case of bi-directional mappings, e.g. from entity to DTO and from DTO to entity, the mapping rules for the forward method and the reverse method are often similar and can simply be inversed by switching source
and target
@Mapper
public interface CarMapper {
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDto carToDto(Car car);
@InheritInverseConfiguration
@Mapping(target = "numberOfSeats", ignore = true)
Car carDtoToCar(CarDto carDto);
}
Shared configurations
MapStruct offers the possibility to define a shared configuration by pointing to a central interface annotated with @MapperConfig
. For a mapper to use the shared configuration, the configuration interface needs to be defined in the @Mapper#config
property. Attributes specified in @Mapper
take precedence over the attributes specified via the referenced configuration class
@MapperConfig(
uses = CustomMapperViaMapperConfig.class,
unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface CentralConfig {
}
@Mapper(config = CentralConfig.class, uses = { CustomMapperViaMapper.class } )
// Effective configuration:
// @Mapper(
// uses = { CustomMapperViaMapper.class, CustomMapperViaMapperConfig.class },
// unmappedTargetPolicy = ReportingPolicy.ERROR
// )
public interface SourceTargetMapper {
...
}
Customizing mappings
If we need to apply custom logic before or after certain mapping methods, MapStruct provides two ways for doing so: decorators which allow for a type-safe customization of specific mapping methods and the before-mapping and after-mapping lifecycle methods which allow for a generic customization of mapping methods with given source or target types.
Mapping customization with before-mapping and after-mapping methods
@Mapper
public abstract class VehicleMapper {
@BeforeMapping
protected void flushEntity(AbstractVehicle vehicle) {
// I would call my entity manager's flush() method here to make sure my entity
// is populated with the right @Version before I let it map into the DTO
}
@AfterMapping
protected void fillTank(AbstractVehicle vehicle, @MappingTarget AbstractVehicleDto result) {
result.fuelUp( new Fuel( vehicle.getTankCapacity(), vehicle.getFuelType() ) );
}
public abstract CarDto toCarDto(Car car);
}
Last updated