1. Introduction

MapStruct is a code generation tool that simplifies mapping between Java bean types. In this article, we’ll explore how to use conditional mapping with MapStruct and look at different configurations to achieve it.

2. Conditional Mapping With MapStruct

When mapping data between objects, we often find the need to map an attribute based on certain conditions, MapStruct offers a few configuration options to achieve this.

Let’s examine an instance of a target License object that requires mapping attributes based on a few conditions:

public class License {
    private UUID id;
    private OffsetDateTime startDate;
    private OffsetDateTime endDate;
    private boolean active;
    private boolean renewalRequired;
    private LicenseType licenseType;

    public enum LicenseType {
        INDIVIDUAL, FAMILY
    }
    // getters and setters
}

The input LicenseDto contains an optional startDate, endDate and licenseType:

public class LicenseDto {
    private UUID id;
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    private String licenseType;
    // getters and setters
}

Here are the mapping rules from LicenseDto to License:

  • id – if the input LicenseDto has id
  • startDate – if the input LicenseDto doesn’t have startDate in the request, let’s set startDate as the current date
  • endDate – if the input LicenseDto doesn’t have endDate in the request, let’s set endDate as one year from the current date
  • active – if the endDate is in the future, we’ll set this to true
  • renewalRequired – if the endDate is in the next two weeks, we’ll set this to true
  • licenseType – if the input licenseType is available and is one of the expected values INDIVIDUAL or FAMILY.

Let’s explore some of the ways we can achieve this by using the configurations provided by MapStruct.

2.1. Using Expressions

MapStruct provides the capability to use any valid Java expression inside a mapping expression to generate the mapping. Let’s utilize this feature to map startDate:

@Mapping(target = "startDate", expression = "java(mapStartDate(licenseDto))")
License toLicense(LicenseDto licenseDto);

We can now define the method mapStartDate:

default OffsetDateTime mapStartDate(LicenseDto licenseDto) {
    return licenseDto.getStartDate() != null ? 
      licenseDto.getStartDate().atOffset(ZoneOffset.UTC) : OffsetDateTime.now();
}

On compilation, MapStruct generates the code:

@Override
public License toLicense(LicenseDto licenseDto) {
    License license = new License();
    license.setStartDate( mapStartDate(licenseDto) );
    
    // Rest of generated mapping...
    return license;
}

Alternatively, without using this method, the ternary operation inside the method can be directly passed into the expression as it’s a valid Java expression.

2.2. Using Conditional Expressions

Similar to an expression, a condition expression is a feature in MapStruct that allows the mapping of an attribute based on a conditional expression inside a string as Java code. The generated code contains the condition inside an if block, Therefore, let’s utilize this feature to map renewalRequired in the License:

@Mapping(target = "renewalRequired", conditionExpression = "java(isEndDateInTwoWeeks(licenseDto))", source = ".")
License toLicense(LicenseDto licenseDto);

We can pass in any valid Java boolean expression inside the java() method. Let’s define the isEndDateInTwoWeeks method inside the mapper interface:

default boolean isEndDateInTwoWeeks(LicenseDto licenseDto) {
    return licenseDto.getEndDate() != null 
       && Duration.between(licenseDto.getEndDate(), LocalDateTime.now()).toDays() <= 14;
}

On compile, MapStruct generates the code to set renewalRequired if this condition is satisfied:

@Override
public License toLicense(LicenseDto licenseDto) {
    License license = new License();

    if ( isEndDateInTwoWeeks(licenseDto) ) {
        license.setRenewalRequired( isEndDateInTwoWeeks( licenseDto ) );
    }
    
    // Rest of generated mapping...

    return license;
}

It’s also possible to set the value of an attribute from the source when a condition matches. In such instances, the mapper will populate the desired attribute with the corresponding value from the source.

2.3. Using Before/After Mapping

In certain situations, if we want to modify the object before or after mapping through customization, we can make use of the @BeforeMapping and @AfterMapping annotations from MapStruct. Let’s use this feature to map endDate:

@Mapping(target = "endDate", ignore = true)
License toLicense(LicenseDto licenseDto);

We can define the AfterMapping annotation to map endDate conditionally. In this way, we can control the mapping based on specific conditions:

@AfterMapping
default void afterMapping(LicenseDto licenseDto, @MappingTarget License license) {
    OffsetDateTime endDate = licenseDto.getEndDate() != null ? licenseDto.getEndDate()
      .atOffset(ZoneOffset.UTC) : OffsetDateTime.now()
      .plusYears(1);
    license.setEndDate(endDate);
}

We need to pass both the input LicenseDto and the target License object as parameters to this afterMapping method. Consequently, this ensures that MapStruct generates the code that invokes this method as a final step of mapping, before returning the License object:

@Override
public License toLicense(LicenseDto licenseDto) {
    License license = new License();

    // Rest of generated mapping...

    afterMapping( licenseDto, license );

    return license;
}

Alternatively, we can use the BeforeMapping annotation instead to achieve the same result.

2.4. Using @Condition

When mapping, we can add a custom presence check to an attribute using @Condition. By default, MapStruct performs a presence check for every attribute, but gives preference to a method annotated with @Condition if available.

Let’s use this feature to map licenseType. The input LicenseDto receives licenseType as a String, and during mapping, we need to map it to the target if it’s not null and resolves to one of the expected enums INDIVIDUAL or FAMILY:

@Condition
default boolean mapsToExpectedLicenseType(String licenseType) {
    try {
        return licenseType != null && License.LicenseType.valueOf(licenseType) != null;
    } catch (IllegalArgumentException e) {
        return false;
    }
}

MapStruct generates the code to use this method mapsToExpectedLicenseType() when mapping licenseType because the signature String matches with the licenseType in LicenseDto:

@Override
public License toLicense(LicenseDto licenseDto) {
    if ( LicenseMapper.mapsToExpectedLicenseType( licenseDto.getLicenseType() ) ) {
        license.setLicenseType( Enum.valueOf( License.LicenseType.class, licenseDto.getLicenseType() ) );
    }

    // Rest of generated mapping...
    return license;
}

3. Conclusion

In this article, we explored different ways to map attributes between Java bean types conditionally using MapStruct.

As always, the source code for the examples is available over on GitHub.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.