How to write MapStruct mappers for object hierarchies

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. This blog elaborates on how to write MapStruct mappers for object hierarchies. If, after reading this, you still wonder what MapStruct is and what it can do for you, then this blog might be a good read: https://techlab.bol.com/mapstruct-case-study/.

To already spoil the fun, MapStruct is not able to generate mappers for classes that all inherit from the same (abstract) base class or interface. In this blog we demonstrate two alternative solutions to write a MapStruct mapper that can. The first approach is based on using – not so elegant - instance-of checks, whereas the second approach relies on the well-established visitor pattern.

Before we demonstrate how to write MapStruct mappers for object hierarchies, let us first start by introducing you to our problem domain. After all, we are discussing a real problem we encountered in the wild.

A gentle introduction to our problem domain

The good thing of using a real scenario is that we can assess if MapStruct actually works in practice. On the downside, we have to bother you with something that might not be as exciting to you as it is to us. If we wouldn’t, the rest of this blog will be as abstract as Foo and Bar. No worries, we will limit the information to what you absolutely need to know.

List pages

Throughout the rest of this blog we use an example domain object model to represent the selection a user made on a list page. If you ever visited our web shop, 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. You can narrow down the selection of products on a page even further by choosing one or more refinements, for example, to select only red sneakers or sneakers within the price range of 25-100 euros.

The domain model

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();
}

Observe that the library needs a full category object, whereas the service’s category only has a category id. How MapStruct deals with that is not important for this blog. (If you are interested, you can read more about that in our other blog: How to map optional fields with MapStruct). What is important, are the refinements. They are defined as a hierarchy of objects. A refinement comes in three flavours:

  • identity refinements,
  • range refinements,
  • delivery period refinements.

An example of an identity refinement is a refinement on colour, for which a user can pick one or more predefined values, e.g. red, green, and blue. An example of a range refinement would be a refinement on price for which a user can select a range of values, e.g. 10-25 euros. An example of a delivery period refinement would be “deliver tomorrow”.

For what follows, the exact definition of each refinement type is not important. So, let’s not bother you with that. What matters is that the list page context has a heterogenous list that can contain any type of aforementioned refinement.

Writing mappers for object hierarchies using instanceof checks

We aim to have only one convert method that takes a refinement of any type and yields a converted refinement of similar type. In other words, a range refinement maps onto a library range refinement and so forth. More concretely, we are looking for this method:

Refinement toLib(RefinementInternal refinement)

In the introduction, we already spoiled it for you. MapStruct currently has no off-the-shelf support for (abstract) base classes (or interfaces) with children (see, for example, this ticket: https://github.com/mapstruct/mapstruct/issues/366). That is to say, simply telling MapStruct that we want a converter for this method signature will not work. The good news is, however, that there is an easy way around this as suggested in the aforementioned ticket.

The solution proposed boils down to having MapStruct generate the conversion method for each concrete refinement. We can then implement the generic conversion method for the refinement base class ourselves. We invoke the appropriate converter method for each subclass by using instance-of checks:

@Mapper
interface RefinementInternalToLibMapper {
    default Refinement toLib(RefinementInternal refinement) {
       if (refinement instanceof IdentityRefinementInternal) {
           return map((IdentityRefinementInternal) refinement);
       } else if (refinement instanceof RangeRefinementInternal) {
           return map((RangeRefinementInternal) refinement);
       } else if (refinement instanceof DeliveryPeriodRefinementInternal) {
           return map((DeliveryPeriodRefinementInternal) refinement);
       } else {
           return null;
       }
    }

    IdentityRefinement map(IdentityRefinementInternal refinement);

    RangeRefinement map(RangeRefinementInternal refinement);

    DeliveryPeriodRefinement map(DeliveryPeriodRefinementInternal refinement);
}

Not a fan of these kinds of instance-of checks? Good, neither are we. Hang on for an alternative solution.

Writing mappers for object hierarchies using the visitor pattern

The main idea of this approach is to use the well-established visitor pattern. We start with declaring an accept method in our refinement interface to welcome any visitor object that desires to visit our refinement:

public interface Refinement {
    void accept(RefinementVisitor refinementVisitor);
}

Now the compiler reminds us to implement the accept method in each concrete refinement. As the visitor pattern prescribes, we delegate to the visit method of the visitor object that is visiting this refinement instance. Consider, for example, the accept method of the identity refinement:

public class IdentityRefinement implements Refinement {
    // Members, getters and setters

    @Override
    public void accept(RefinementVisitor refinementVisitor) {
        refinementVisitor.visit(this);
    }
}

Of course, this whole exercise is pretty pointless without having a visitor that can actually visit the refinements. So, we start by defining an abstract visitor interface:

public interface RefinementVisitor<T> {
    T visit(RangeRefinementInternalDC refinement);

    T visit(DeliveryPeriodRefinementInternalDC refinement);

    T visit(IdentityRefinementInternalDC refinement);
}

Last step is to implement the mapper as a concrete instance of a refinement visitor:

@Mapper
public abstract class RefinementInternalToLibMapper implements RefinementVisitor<Refinement> {
    Refinement toLib(RefinementInternal refinement) {
       return refinement.accept(this);
    }

    @Override
    public Refinement visit(IdentityRefinementInternal r) {
        return map(r);
    }

    @Override
    public Refinement visit(RangeRefinementInternal r) {
        return map(r);
    }

    @Override
    public Refinement visit(NotAvailableRefinementInternal r) {
        return map(r);
    }

    @Override
    public Refinement visit(DeliveryPeriodRefinementInternal r) {
        return map(r);
    }

    abstract IdentityRefinement map(IdentityRefinementInternal r);

    abstract RangeRefinement map(RangeRefinementInternal r);

    abstract DeliveryPeriodRefinement map(DeliveryPeriodRefinementInternal r);
}

Just like in the previous approach, we have MapStruct implement the mapping code for each concrete refinement. Differently from the instance-of approach, we only have to call the refinement’s accept method. Java figures out which exact method to call at runtime (based on polymorphism) by which we avoid those instance-of checks. Recall the implementation of the accept method which, in this case, invokes the mapper’s visit method. As can be seen in the implementation above, all visit methods are implemented the same; calling the map method for this particular type of refinement. Works like a charm.

Conclusion

In this blog we have shown you how to write MapStruct mappers for object hierarchies. We demonstrated two different approaches: the first approach uses instance-of checks - something we are not that fond of, whereas the second relies on the well-known visitor pattern.

Of course, the visitor pattern requires more lines of code compared to the instance-of approach shown earlier. At the very least, we argue that this way of implementing the converter is based on a well-established and well-known design pattern. The visitor pattern might be used for other purposes too. Indeed, in our particular case, we already implemented the visitor pattern to accommodate for executing other operations on the refinement hierarchy. Therefore, applying this pattern to the converter felt like a natural thing to do. Whatever approach appeals the most - eventually it's completely up to you. At least you have a selection to choose from now.

Tags

Nick Tinnemeier

All articles by me