How to map optional fields with MapStruct

Introduction

MapStruct is a framework that alleviates us, programmers, from the unexciting task of writing code to copy one object model to another, field by field. In this blog we show you how to map optional fields with MapStruct. If, after reading this, you still wonder what MapStruct is and what it can do for you, then this blog could answer those questions for you: https://techlab.bol.com/mapping-object-models-case-study/.

This blog zooms in on MapStruct’s capabilities of dealing with source fields that are not always present, i.e. might be null. In particular, we show you:

  1. how to write mappers for Java optionals, and
  2. how to create mappers that map multiple source objects onto one target object, in case one of those sources can be null.

For each case, we explain which issues you might run into, and of course, how to overcome them. However, first we provide you with a whirlwind introduction to our problem domain because we like to have our examples a bit more realistic than Foo and Bar.

A whirlwind introduction to our problem domain

Throughout the rest of this blog, we use an example domain object model to represent the selection a user has made on a list page. If you have ever visited our webshop, you probably know what a list page is. A list page is a list of products you can browse through. On a list page, you can optionally select a category, such as sneakers, to only display casual, sporty shoes. If you already know what you’re looking for and don’t want to browse through thousands of sneakers, you can limit your selection further by refining on, for example only red sneakers or sneakers within the price range of 25-100 euros.

In our case, we convert classes that are used inside a REST service to classes that live in a library used by this service. The ListPageContextInternal object that is used internally in our REST service represents the user’s selection on a list page: an optional category id and a possibly empty list of refinements:

public interface ListPageContextInternal {
    Optional<Long> getCategoryId();
    List<RefinementInternal> getRefinements();
}

We need to convert this object to another object model that is defined in a library. This object model almost looks the same:

public interface ListPageContext {
    Optional<Category> getCategory();
    List<RefinementInternal> getRefinements();
}

Take note that the library needs a full category object, whereas the service’s category only has a category id. Our service fetches a full category object that can be provided to the library list page context. Again, the category object model as defined in the service resembles the one implemented in the library, but the objects are not the same. This makes for an interesting use case, as we need to map two source objects to one target object. More about that later.

Mapping Java optionals

Let’s write a mapper that maps the internal list page context and a category object to a library list page context. Recall that we wrap the non-mandatory category in an Optional class. That is to say, on the library’s list page context, we define the category as an optional field:

Optional<Category> category

Wouldn’t it be nice if MapStruct supports that? In our first attempt, we write a mapper which takes two arguments: the list page context and a (non-nullable) category. This is what that looks like:

@Mapper
public interface ListPageContextToLibMapper {
    @Mapping
    ListPageContext toLib(ListPageContextInternal context, Category category);
}

When we compile this code, MapStruct gently informs us that we’re doing it completely wrong:

Can't map parameter "Category category" to "java.util.Optional<? extends Category> category".

A mapper for mapping optionals

MapStruct is indeed smart enough to recognise that the category parameter of our mapper method corresponds to the category field of the library list page context object. It just does not know how to convert the category to an optional category. Fortunately, MapStruct gives us a hint on how to make it work:

Consider to declare/implement a mapping method: "java.util.Optional<? extends Category> map(Category value)".

Well, thanks for the advice MapStruct. Would this perhaps work for you?

@Mapper
public interface ListPageContextInternalToLibMapper {
    @Mapping
    ListPageContext toLib(ListPageContextInternal context, Category category);

    default Optional<Category> wrapOptional(Category category) {
        return Optional.of(category);
    }
}

It does work. MapStruct generates the mapper and while looking at the generated code we see that MapStruct calls the wrapOptional method just before setting the field:

listPageContext.category(wrapOptional(category));

A reusable mapper for mapping optionals

At this point, you might wonder - just like us - if we could make this more generic and thus reusable for any optional field. By replacing the Category type with a type parameter, MapStruct still recognises that it should use this method for converting the category to an optional category. The wrapOptional method then looks as follows:

default <T> Optional<T> wrapOptional(T object) {
    return Optional.of(object);
}

It is interesting to note that his solution works equally well for the case in which you map a non-optional attribute of the source object to an optional attribute of the target object. Furthermore, note that an unwrap method to deal with the case where you need to convert an optional field to a non-optional one can be written in a similar fashion.

We have shown you that MapStruct does not support optionals out of the box. By this example, we hope to have laid out that writing some boilerplate code to handle optionals in a natural way is fairly easy. The good news is that out of the box support for optionals seems to be on its way: https://github.com/mapstruct/mapstruct/issues/674.

Mapping multiple (optional) sources onto one target

Up till now, we assumed that the second argument in our mapping method – the category – is always present. For a reason, as it turns out, things do get hairy when one of the sources can be null. Here we show you what problems we ran into and, of course, how we fixed them. Remember our previous mapper converting a list page context to its library counterpart:

@Mapper
public interface ListPageContextInternalToLibMapper {
    @Mapping
    ListPageContext toLib(ListPageContextInternal context, Category category);

    default Optional<Category> wrapOptional(Category category) {
        return Optional.of(category);
    }
}

Where things fail

Now let’s drop the assumption that the category is not nullable. In that case, we have to change the wrapOptional method slightly to use Optional.ofNullable instead of Optional.of and we are good to go. Well, are we? MapStruct happily generates the mapping code for us, like it always does, except that this time it is not what we expected:

public ListPageContext toLib(ListPageContextInternal context, Category category) {
    if ( context == null && category == null ) {
        return null;
    }

    Builder listPageContext = ListPageContext.builder();
 
    // Mapping code for listPageContext fields

    if ( category != null ) {
        listPageContext.category( wrapOptional( category ) );
    }

    return listPageContext.build();
}

Did you spot that null check there on line 10? As long as the category is present, everything is fine. If it is null, however, the category will not be set as an empty optional on the list page context. That is not ideal, so let’s find a way to overcome this issue.

Where things fail even more

MapStruct provides many parameters that can be provided to the @Mapping annotation, each influencing the code that will be generated in a different way. The ‘defaultExpression’ can be used to specify a Java expression providing a value that should be used if the source field is null. That sounds useable, so let’s try with that:

@Mapping(target = "category", 
         source = "category",
         defaultExpression = "java(java.util.Optional.empty())")
ListPageContext toLib(ListPageContextInternal context, Category category);

Here we define the empty optional to be used as the default value for when the category is null. Note that we have to use the full package name to refer to Java’s optional class. Fair enough, we cannot expect MapStruct to figure out itself which import to use, can we? Based on this specification MapStruct generates the following mapper:

public ListPageContext toLib(ListPageContextInternal context, Category category) {
    if ( context == null && category == null ) {
        return null;
    }

    Builder listPageContext = ListPageContext.builder();
 
    // Mapping code for listPageContext fields

    if ( category != null ) {
        if ( category != null ) {
            listPageContext.category( wrapOptional( category ) );
        }
        else {
            listPageContext.category( java.util.Optional.empty() );
        }
    }

    return listPageContext.build();
}

Yikes, that did not quite fix our problem, did it? It only made things worse. Wait, don’t bail out yet, there are ways to make it work. So let’s appreciate MapStruct for the things it can do.

Failure is not an option: using expressions

Previously we used the concept of a default expression. A default expression only provides a value for the case the field is null. An expression, on the other hand, defines the value a field should always have. Now, we can overcome our issue by introducing two mapping methods. The first method only takes a list page context and defines the value for the absent category by means of an expression. The second method takes a non-nullable category object as we did before:

@Mapper
public interface ListPageContextInternalToLibMapper {
    @Mapping(target = "category", expression = "java(java.util.Optional.empty())")
    ListPageContext toLib(ListPageContextInternal context);

    @Mapping(target = "category", source = "category)
    ListPageContext toLib(ListPageContextInternal context, Category category);

    default <T> Optional<T> wrapOptional(T t) {
        return Optional.of(t);
    }
}

This will produce the following code for the first mapping method, that does actually work:

@Override
public ListPageContext toLib(ListPageContextInternal context) {

    Builder listPageContext = ListPageContext.builder();

    if ( context != null ) {
        // Mapping code for listPageContext fields
    }

    listPageContext.category( java.util.Optional.empty() );

    return listPageContext.build();
}

The code for the second mapping method which takes a non-null category object remains the same as before. Yes, it still contains that null check on category, which is redundant. We couldn’t find any way to avoid the null check. It doesn’t hurt either, so we guess that is just something we have to live with. The con of this approach is that we have two mapping methods instead of one. This forces any code using the mapper to do a null check and decide which mapping method to invoke. Wouldn’t it be nice if we just had one mapping method that does the null check for us? Let’s do just that.

Failure is not an option: using after mappings

MapStruct provides means to inject custom code in the mapping method. You can do so at the beginning of the method - right before mapping starts - or at the end - just before returning the mapped object. This workaround is based on injecting code at the end of the mapping method. We enrich the mapping code with the code we expected MapStruct to write for us when we used the default expression, that is setting the value of the category to an empty optional if the source category equals null:

@Mapper
public interface ListPageContextInternalToLibMapper {
    @Mapping(source = "category", target = "category")
    ListPageContext toLib(ListPageContextInternal context, Category category);

    @AfterMapping
    default void afterMap(@MappingTarget ListPageContext.Builder context, 
                          Category category) {
        if ( category == null ) {
            context.category(Optional.empty());
        } 
    }

    default <T> Optional<T> wrapOptional(T t) {
        return Optional.of(t);
    }
}

We start with implementing a method that takes the builder of that target object we are constructing and the category source object. To instruct MapStruct to inject and execute this method at the end of the mapping method we annotate it with a @AfterMapping annotation. All this gives us the following mapper implementation, that actually does what we desire:

public ListPageContext toLib(ListPageContextInternal context, Category category) {
    if ( context == null && category == null ) {
        return null;
    }

    Builder listPageContext = ListPageContext.builder();
 
    // Mapping code for listPageContext fields


    if ( category != null ) {
        listPageContext.category( wrapOptional( category ) );
    }

    afterMap( listPageContext, category );

    return listPageContext.build();
}

Conclusion

In this blog post, we have shown you how to map optional fields with MapStruct. In particular, we revealed that MapStruct does not support converting to Java optionals out-of-the-box. We have also laid out how to overcome this by writing a tiny bit of boilerplate code. Moreover, we discussed the problems you could run into when mapping multiple nullable source objects to one target object. Of course, you won’t run into them anymore because we have also demonstrated workarounds to avoid these issues.

Nick Tinnemeier

All articles by me