Ignoring Unmapped Properties with MapStruct

1. Overview

In Java applications, we may wish to copy values from one type of Java bean to another. To avoid long, error-prone code, we can use a bean mapper such as MapStruct.

While mapping identical fields with identical field names is very straightforward, we often encounter mismatched beans. In this tutorial, we’ll look at how MapStruct handles partial mapping.

2. Mapping

MapStruct is a Java annotation processor. Therefore, all we need to do is to define the mapper interface and to declare mapping methods. MapStruct will generate an implementation of this interface during compilation.

For simplicity, let’s start with two classes with the same field names:

public class CarDTO {
    private int id;
    private String name;
}
public class Car {
    private int id;
    private String name;
}

Next, let’s create a mapper interface:

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDTO carToCarDTO(Car car);
}

Finally, let’s test our mapper:

@Test
public void givenCarEntitytoCar_whenMaps_thenCorrect() {
    Car entity = new Car();
    entity.setId(1);
    entity.setName("Toyota");

    CarDTO carDto = CarMapper.INSTANCE.carToCarDTO(entity);

    assertThat(carDto.getId()).isEqualTo(entity.getId());
    assertThat(carDto.getName()).isEqualTo(entity.getName());
}

3. Unmapped Properties

As MapStruct operates at compile time, it can be faster than a dynamic mapping framework. It can also generate error reports if mappings are incomplete — that is, if not all target properties are mapped:

Warning:(X,X) java: Unmapped target property: "propertyName".

While this is is a helpful warning in the case of an accident, we may prefer to handle things differently if the fields are missing on purpose.

Let’s explore this with an example of mapping two simple objects:

public class DocumentDTO {
    private int id;
    private String title;
    private String text;
    private List<String> comments;
    private String author;
}
public class Document {
    private int id;
    private String title;
    private String text;
    private Date modificationTime;
}

We have unique fields in both classes that are not supposed to be filled during mapping. They are:

  • comments in DocumentDTO

  • author in DocumentDTO

  • modificationTime in Document

If we define a mapper interface, it will result in warning messages during the build:

@Mapper
public interface DocumentMapper {
    DocumentMapper INSTANCE = Mappers.getMapper(DocumentMapper.class);

    DocumentDTO documentToDocumentDTO(Document entity);
    Document documentDTOToDocument(DocumentDTO dto);
}

As we do not want to map these fields, we can exclude them from mapping in a few ways.

4. Ignoring Specific Fields

To skip several properties in a particular mapping method, we can use the ignore property in the @Mapping annotation:

@Mapper
public interface DocumentMapperMappingIgnore {

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

    @Mapping(target = "comments", ignore = true)
    @Mapping(target = "author", ignore = true)
    DocumentDTO documentToDocumentDTO(Document entity);

    @Mapping(target = "modificationTime", ignore = true)
    Document documentDTOToDocument(DocumentDTO dto);
}

Here, we’ve provided the field name as the target and set ignore to true to show that it’s not required for mapping.

However, this technique is not convenient for some cases. We may find it difficult to use, for example, when using big models with a large number of fields.

5. Unmapped Target Policy

To make things clearer and the code more readable, we can specify the unmapped target policy.

To do this, we use the MapStruct unmappedTargetPolicy to provide our desired behavior when there is no source field for the mapping:

  • ERROR: any unmapped target property will fail the build – this can help us avoid accidentally unmapped fields

  • WARN: (default) warning messages during the build

  • IGNORE: no output or errors

In order to ignore unmapped properties and get no output warnings, we should assign the IGNORE value to the unmappedTargetPolicy. There are several ways to do it depending on the purpose.

5.1. Set a Policy on Each Mapper

We can set the unmappedTargetPolicy to the @Mapper annotation. As a result, all its methods will ignore unmapped properties:

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface DocumentMapperUnmappedPolicy {
    // mapper methods
}

5.2. Use a Shared MapperConfig

We can ignore unmapped properties in several mappers by setting the unmappedTargetPolicy via @MapperConfig to share a setting across several mappers.

First we create an annotated interface:

@MapperConfig(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface IgnoreUnmappedMapperConfig {
}

Then we apply that shared configuration to a mapper:

@Mapper(config = IgnoreUnmappedMapperConfig.class)
public interface DocumentMapperWithConfig {
    // mapper methods
}

We should note that this is a simple example showing the minimal usage of @MapperConfig, which might not seem much better than setting the policy on each mapper. The shared config becomes very useful when there are multiple settings to standardize across several mappers.

5.3. Configuration Options

Finally, we can configure the MapStruct code generator’s annotation processor options. When using Maven, we can pass processor options using the compilerArgs parameter of the processor plug-in:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${maven-compiler-plugin.version}</version>
            <configuration>
                <source>${maven.compiler.source}</source>
                <target>${maven.compiler.target}</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
                <compilerArgs>
                    <compilerArg>
                        -Amapstruct.unmappedTargetPolicy=IGNORE
                    </compilerArg>
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>

In this example, we’re ignoring the unmapped properties in the whole project.

6. The Order of Precedence

We’ve looked at several ways that can help us to handle partial mappings and completely ignore unmapped properties. We’ve also seen how to apply them independently on a mapper, but we can also combine them.

Let’s suppose we have a large codebase of beans and mappers with the default MapStruct configuration. We don’t want to allow partial mappings except in a few cases. We might easily add more fields to a bean or its mapped counterpart and get a partial mapping without even noticing it.

So, it’s probably a good idea to add a global setting through Maven configuration to make the build fail in case of partial mappings.

Now, in order to allow unmapped properties in some of our mappers and override the global behavior, we can combine the techniques, keeping in mind the order of precedence (from highest to lowest):

  • Ignoring specific fields at the mapper method-level

  • The policy on the mapper

  • The shared MapperConfig

  • The global configuration

7. Conclusion

In this tutorial, we looked at how to configure MapStruct to ignore unmapped properties.

First, we looked at what unmapped properties mean for mapping. Then we saw how partial mappings could be allowed without errors, in a few different ways.

Finally, we learned how to combine these techniques, keeping in mind their order of precedence.

As always, the code from this tutorial is available over on GitHub.