Preface

This is the reference documentation of the MapStruct Spring Extensions, an annotation processor designed to complement the core MapStruct project with features specific to the Spring Framework. This guide covers all the functionality provided by the MapStruct Spring Extensions. In case this guide doesn’t answer all your questions just join the MapStruct Google group to get help.

This guide assumes some familiarity with the MapStruct core. If this is your first introduction to MapStruct, you might wish to start with the core documentation.

You found a typo or other error in this guide? Please let us know by opening an issue in the MapStruct Spring Extensions GitHub repository, or, better yet, help the community and send a pull request for fixing it!

1. Introduction

The MapStruct Spring Extensions are a Java annotation processor extending the well known MapStruct project with features specific to the Spring Framework.

All you have to do is to define your MapStruct mapper to extend Spring’s Converter interface. During compilation, the extensions will generate an adapter which allows the standard MapStruct mappers to use Spring’s ConversionService.

This enables the developer to define MapStruct mappers with only the ConversionService in their uses attribute rather than having to import every single Mapper individually, thus allowing for looser coupling between Mappers.

2. Set up

MapStruct Spring Extensions is a Java annotation processor based on JSR 269 and as such can be used within command line builds (javac, Ant, Maven etc.) as well as from within your IDE. The minimum JDK version is 11.

Also, you will need MapStruct itself (at least version 1.4.0.Final) in your project.

It comprises the following artifacts:

  • org.mapstruct.extensions.spring:mapstruct-spring-annotations: contains the added annotations such as @SpringMapperConfig

  • org.mapstruct.extensions.spring:mapstruct-spring-extensions: contains the annotation processor which generates Spring components

2.1. Apache Maven

For Maven based projects add the following to your POM file in order to use MapStruct Spring Extensions:

Example 1. Maven configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...
<properties>
    <org.mapstruct.extensions.spring.version>1.1.2</org.mapstruct.extensions.spring.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct.extensions.spring</groupId>
        <artifactId>mapstruct-spring-annotations</artifactId>
        <version>${org.mapstruct.extensions.spring.version}</version>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct.extensions.spring</groupId>
                        <artifactId>mapstruct-spring-extensions</artifactId>
                        <version>${org.mapstruct.extensions.spring.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
...

If you wish to use the test extensions, additionally add the following:

1
2
3
4
5
6
7
8
9
10
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct.extensions.spring</groupId>
        <artifactId>mapstruct-spring-test-extensions</artifactId>
        <scope>test</scope>
        <version>${org.mapstruct.extensions.spring.version}</version>
    </dependency>
</dependencies>
...

If you are working with the Eclipse IDE, make sure to have a current version of the M2E plug-in. When importing a Maven project configured as shown above, it will set up the MapStruct Spring Extensions annotation processor so it runs right in the IDE, whenever you save a mapper type extending a Spring converter. Neat, isn’t it?

To double check that everything is working as expected, go to your project’s properties and select "Java Compiler" → "Annotation Processing" → "Factory Path". The MapStruct Spring Extensions JAR should be listed and enabled there. Any processor options configured via the compiler plug-in (see below) should be listed under "Java Compiler" → "Annotation Processing".

If the processor is not kicking in, check that the configuration of annotation processors through M2E is enabled. To do so, go to "Preferences" → "Maven" → "Annotation Processing" and select "Automatically configure JDT APT". Alternatively, specify the following in the properties section of your POM file: <m2e.apt.activation>jdt_apt</m2e.apt.activation>.

Also make sure that your project is using Java 1.8 or later (project properties → "Java Compiler" → "Compile Compliance Level"). It will not work with older versions.

2.2. Gradle

Add the following to your Gradle build file in order to enable MapStruct Spring Extensions:

Example 2. Gradle configuration (5.2 and later)
1
2
3
4
5
6
7
8
9
10
11
...

dependencies {
    ...
    implementation "org.mapstruct.extensions.spring:mapstruct-spring-annotations:${mapstructSpringExtensionsVersion}"
    annotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"

    // If you are using MapStruct Spring Extensions in test code
    testAnnotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"
}
...

And for test extensions:

1
2
3
4
5
6
7
...

dependencies {
    ...
    testImplementation "org.mapstruct.extensions.spring:mapstruct-spring-test-extensions:${mapstructSpringExtensionsVersion}"
}
...
Example 3. Gradle configuration (3.4 - 5.1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
plugins {
    ...
    id 'net.ltgt.apt' version '0.20'
}

// You can integrate with your IDEs.
// See more details: https://github.com/tbroyer/gradle-apt-plugin#usage-with-ides
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'

dependencies {
    ...
    implementation "org.mapstruct.extensions.spring:mapstruct-spring-annotations:${mapstructSpringExtensionsVersion}"
    annotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"

    // If you are using MapStruct Spring Extensions in test code
    testAnnotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"
}
...

And for test extensions:

1
2
3
4
5
6
7
...

dependencies {
    ...
    testImplementation "org.mapstruct.extensions.spring:mapstruct-spring-test-extensions:${mapstructSpringExtensionsVersion}"
}
...
Example 4. Gradle (3.3 and older)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
plugins {
    ...
    id 'net.ltgt.apt' version '0.20'
}

// You can integrate with your IDEs.
// See more details: https://github.com/tbroyer/gradle-apt-plugin#usage-with-ides
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'

dependencies {
    ...
    compile "org.mapstruct.extensions.spring:mapstruct-spring-annotations:${mapstructSpringExtensionsVersion}"
    annotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"

    // If you are using MapStruct Spring Extensions in test code
    testAnnotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"
}
...

And for test extensions:

1
2
3
4
5
6
7
...

dependencies {
    ...
    testCompile "org.mapstruct.extensions.spring:mapstruct-spring-test-extensions:${mapstructSpringExtensionsVersion}"
}
...

2.3. Apache Ant

Add the javac task configured as follows to your build.xml file in order to enable MapStruct Spring Extensions in your Ant-based project. Adjust the paths as required for your project layout.

Example 5. Ant configuration
1
2
3
4
5
6
7
8
9
...
<javac
    srcdir="src/main/java"
    destdir="target/classes"
    classpath="path/to/mapstruct-spring-annotations1.1.2.jar">
    <compilerarg line="-processorpath path/to/mapstruct-spring-extensions-1.1.2.jar"/>
    <compilerarg line="-s target/generated-sources"/>
</javac>
...

3. Mappers as Converters

MapStruct Mappers nicely match Spring’s Converter idea:

1
2
3
4
5
@Mapper(componentModel = "spring")
public interface CarMapper extends Converter<Car, CarDto> {
    @Mapping(target = "seats", source = "seatConfiguration")
    CarDto convert(Car car);
}

This allows using the Mapper indirectly via the ConversionService:

1
2
3
4
5
6
...
    @Autowired
    private ConversionService conversionService;
...
    Car car = ...;
    CarDto carDto = conversionService.convert(car, CarDto.class);

All this can be achieved already with MapStruct’s core functionality. However, when a Mapper wants to invoke another one, it can’t take the route via the ConversionService, because the latter’s convert method does not match the signature that MapStruct expects for a mapping method. Thus, the developer still has to add every invoked Mapper to the invoking Mapper’s uses element. This creates (aside from a potentially long list) a tight coupling between Mappers that the ConversionService is designed to avoid.

This is where MapStruct Spring Extensions can help. Including the two artifacts in your build will generate an Adapter class that can be used by an invoking Mapper. Let’s say that the above CarMapper is accompanied by a SeatConfigurationMapper:

1
2
3
4
5
6
@Mapper
public interface SeatConfigurationMapper extends Converter<SeatConfiguration, SeatConfigurationDto> {
    @Mapping(target = "seatCount", source = "numberOfSeats")
    @Mapping(target = "material", source = "seatMaterial")
    SeatConfigurationDto convert(SeatConfiguration seatConfiguration);
}

The generated Adapter class will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class ConversionServiceAdapter {
  private final ConversionService conversionService;

  public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
    this.conversionService = conversionService;
  }

  public CarDto mapCarToCarDto(final Car source) {
    return (CarDto) conversionService.convert(source, TypeDescriptor.valueOf(Car.class), TypeDescriptor.valueOf(CarDto.class));
  }

  public SeatConfigurationDto mapSeatConfigurationToSeatConfigurationDto(
      final SeatConfiguration source) {
    return (SeatConfigurationDto) conversionService.convert(source, TypeDescriptor.valueOf(SeatConfiguration.class), TypeDescriptor.valueOf(SeatConfigurationDto.class));
  }
}

Since this class' methods match the signature that MapStruct expects, we can now add it to the CarMapper:

1
2
3
4
5
@Mapper(uses = ConversionServiceAdapter.class)
public interface CarMapper extends Converter<Car, CarDto> {
    @Mapping(target = "seats", source = "seatConfiguration")
    CarDto convert(Car car);
}

3.1. Custom Names

By default, the generated class will be located in the package org.mapstruct.extensions.spring.converter and receive the name ConversionServiceAdapter. Typically, you will want to change these names, most often at least the package. This can be accomplished by adding the SpringMapperConfig annotation on any class within your regular source code. One natural candidate would be your shared configuration if you use this:

1
2
3
4
5
6
7
8
import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.SpringMapperConfig;
import org.mapstruct.extensions.spring.example.adapter.MyAdapter;

@MapperConfig(componentModel = "spring", uses = MyAdapter.class)
@SpringMapperConfig(conversionServiceAdapterPackage ="org.mapstruct.extensions.spring.example.adapter", conversionServiceAdapterClassName ="MyAdapter")
public interface MapperSpringConfig {
}

Note: If you do not specify the conversionServiceAdapterPackage element, the generated Adapter class will reside in the same package as the annotated Config.

3.2. Specifying The Conversion Service Bean Name

If your application has multiple ConversionService beans, you will need to specify the bean name. The SpringMapperConfig allows you to specify it using the conversionServiceBeanName property.

1
2
3
4
5
6
7
import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.SpringMapperConfig;

@MapperConfig(componentModel = "spring", uses = ConversionServiceAdapter.class)
@SpringMapperConfig(conversionServiceBeanName = "myConversionService")
public interface MapperSpringConfig {
}

3.2.1. Modified ConverterScan

When the conversionServiceBeanName property is set, the built-in ConverterScan cannot be used in tests as it does not pick up this property. However, setting the property generateConverterScan to true will create an alternative inside the project. Important to note: This version will not create a ConversionService with the given bean name, but merely register all Mappers with the bean identified by the given name. This leads to two practical differences:

  • Unlike its Test Extensions counterpart, this version is perfectly suited to be used in production code.

  • In a test, the developer will still have to provide a ConfigurableConversionService themselves, e.g.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ExtendWith(SpringExtension.class)
public class ConversionServiceAdapterIntegrationTest {
  @Configuration
  @ConverterScan
  static class AdditionalBeanConfiguration {
    @Bean
    ConfigurableConversionService myConversionService() {
      return new DefaultConversionService();
    }
  }

  @Autowired
  @Qualifier("myConversionService")
  private ConversionService conversionService;
}

3.3. Modifying the name for the generated adapter method

By default, the adapter class will contain method names of the form map<SourceTypeName>To<targetTypeName>. If you wish to change this, you can do so on a per-Mapper basis by applying the annotation @AdapterMethodName:

1
2
3
4
5
6
@Mapper(config = MapperSpringConfig.class)
@AdapterMethodName("toDto")
public interface WheelMapper extends Converter<Wheel, WheelDto> {
    @Override
    WheelDto convert(Wheel source);
}

This changes the generated method name to be the annotation’s value attribute:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class ConversionServiceAdapter {
private final ConversionService conversionService;

  public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
    this.conversionService = conversionService;
  }

  public WheelDto toDto(final Wheel source) {
    return (WheelDto) conversionService.convert(source, TypeDescriptor.valueOf(Wheel.class), TypeDescriptor.valueOf(WheelDto.class));
  }
}

4. External Conversions

Spring ships with a variety of builtin conversions, e.g. String to Locale or Object to Optional. In order to use these (or your own conversions from another module) in the same fashion, you can add them as externalConversions to your SpringMapperConfig:

1
2
3
4
5
6
7
8
9
10
import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.ExternalConversion;
import org.mapstruct.extensions.spring.SpringMapperConfig;

import java.util.Locale;

@MapperConfig(componentModel = "spring")
@SpringMapperConfig(
    externalConversions = @ExternalConversion(sourceType = String.class, targetType = Locale.class))
public interface MapstructConfig {}

The processor will add the corresponding methods to the generated adapter so MapStruct can use them in the same fashion as the ones for the Converter Mappers in the same module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.String;
import java.util.Locale;
import javax.annotation.Generated;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.extensions.spring.converter.ConversionServiceAdapterGenerator",
    date = "2021-06-25T18:51:21.585Z"
)
@Component
public class ConversionServiceAdapter {
  private final ConversionService conversionService;

  public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
    this.conversionService = conversionService;
  }

  public Locale mapStringToLocale(final String source) {
    return (Locale) conversionService.convert(source, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Locale.class));
  }
}

4.1. Modifying the name for the generated adapter method

By default, the adapter class will contain method names of the form map<SourceTypeName>To<targetTypeName>. If you wish to change this, you can do so on a per-conversion basis by setting the property adapterMethodName:

1
2
3
4
@MapperConfig(componentModel = "spring")
@SpringMapperConfig(
    externalConversions = @ExternalConversion(sourceType = Blob.class, targetType = byte[].class, adapterMethodName = "blob2Bytes"))
public interface MapstructConfig {}

This changes the generated method name to be the property’s value:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class ConversionServiceAdapter {
  private final ConversionService conversionService;

  public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
    this.conversionService = conversionService;
  }

  public byte[] blob2Bytes(final Blob source) {
    return (byte[]) conversionService.convert(source, TypeDescriptor.valueOf(Blob.class), TypeDescriptor.valueOf(byte[].class));
  }
}

5. Test Extensions

5.1. ConverterScan

In a production environment, Spring will take care of initializing the ConversionService and registering all generated Mappers inside it. However, in integration tests, the developer will typically have to take care of all that themselves. In order to simplify this task, the test-extensions module provides a @ConverterScan annotation which can be used much like Spring’s own @ComponentScan. It will perform the same scanning task and also provide a ConversionService with all found Mappers already registered inside. This is sufficient for most tests. In its simplest form, the annotation can be used like this:

1
2
3
4
5
6
7
8
9
10
11
@ExtendWith(SpringExtension.class)
class ConversionServiceAdapterIntegrationTest {
  @Configuration
  @ConverterScan(basePackageClasses = MapperSpringConfig.class)
  static class ScanConfiguration {}

  @Autowired
  private ConversionService conversionService;

  [...]
}

6. Delegating Converters

Applying MapStruct’s inverse mappings requires a second mapping method inside a @Mapper-annotated interface or abstract class. Since this is not how Spring sees Converter s, this inverted mapping method will be "invisible" to the ConversionService. Adding the annotation @DelegatingConverter to the same method will lead to the generation of a separate class which implements Converter and does nothing more than call the annotated method in its own convert. This class will have a method counterpart in the generated Adapter just like the ones annotated with @Mapper.

Take this Converter for example:

1
2
3
4
5
6
7
8
9
@Mapper(config = MapperSpringConfig.class)
public interface CarMapper extends Converter<Car, CarDto> {
    @Mapping(target = "seats", source = "seatConfiguration")
    CarDto convert(Car car);

    @InheritInverseConfiguration
    @DelegatingConverter
    Car invertConvert(CarDto carDto);
}

Notice the combination of @InheritInverseConfiguration and @DelegatingConverter on the invertConvert method. The @DelegatingConverter will lead to a class like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class CarDtoToCarConverter implements Converter<CarDto, Car> {
  private CarMapper delegateMapper;

  public CarDtoToCarConverter(@Autowired final CarMapper delegateMapper) {
    this.delegateMapper = delegateMapper;
  }

  @Override
  public Car convert(final CarDto source) {
    return delegateMapper.invertConvert(source);
  }
}

The generated Adapter will contain a method counterpart like this:

1
2
3
  public Car mapCarDtoToCar(final CarDto source) {
    return (Car) conversionService.convert(source, TypeDescriptor.valueOf(CarDto.class), TypeDescriptor.valueOf(Car.class));
  }

Please note: The behaviour of @DelegatingConverter is not tied to @InheritInverseConfiguration; however, this is the only use case we are aware of where it provides meaningful value.