1. Overview

MapStruct is a Java annotation processor that comes in handy when generating type-safe and effective mappers for Java bean classes.

In this tutorial, we’ll specifically learn how to use the Mapstruct mappers with Java bean classes which are inherited.

We’ll discuss three approaches. The first approach is instance-checks, while the second is to use the Visitor pattern. The final and recommended approach is to use the @SubclassMapping annotation introduced in Mapstruct 1.5.0.

2. Maven Dependency

Let’s add the following mapstruct dependency to our Maven pom.xml:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.6.0.Beta1</version>
</dependency>

3. Understanding the Problem

Mapstruct, by default, isn’t intelligent enough to have mappers generated for classes that all inherit from the base class or interface. Mapstruct also doesn’t support identifying the provided instance under object hierarchies at runtime.

3.1. Creating POJOs

Let’s suppose our Car and Bus POJO classes extend the Vehicle POJO class:

public abstract class Vehicle {
    private String color;
    private String speed;
    // standard constructors, getters and setters
}
public class Car extends Vehicle {
    private Integer tires;
    // standard constructors, getters and setters
}
public class Bus extends Vehicle {
    private Integer capacity;
    // standard constructors, getters and setters
}

3.2. Creating the DTOs

Let’s have our CarDTO class and BusDTO class extend the VehicleDTO class:

public class VehicleDTO {
    private String color;
    private String speed;
    // standard constructors, getters and setters
}
public class CarDTO extends VehicleDTO {
    private Integer tires;
    // standard constructors, getters and setters
}
public class BusDTO extends VehicleDTO {
    private Integer capacity;
    // standard constructors, getters and setters
}

3.3. The Mapper Interface

Let’s define the base mapper interface, VehicleMapper, using the subclass mappers:

@Mapper(uses = { CarMapper.class, BusMapper.class })
public interface VehicleMapper {
    VehicleDTO vehicleToDTO(Vehicle vehicle);
}
@Mapper()
public interface CarMapper {
    CarDTO carToDTO(Car car);
}
@Mapper()
public interface BusMapper {
    BusDTO busToDTO(Bus bus);
}

Here, we’ve separately defined all the subclass mappers and used those when generating the base mapper. These subclass mappers could either be handwritten or generated by Mapstruct. In our use case, we’re using Mapstruct-generated subclass mappers.

3.4. Identifying the Problem

After we generate the implementation class under /target/generated-sources/annotations/, let’s write a test case to verify whether our base mapper, VehicleMapper, can dynamically map to the correct subclass DTO based on the provided subclass POJO instance.

We’ll test this by providing a Car object and verifying whether the generated DTO instance is of type CarDTO:

@Test
void whenVehicleTypeIsCar_thenBaseMapperNotMappingToSubclass() {
    Car car = getCarInstance();

    VehicleDTO vehicleDTO = vehicleMapper.vehicleToDTO(car);
    Assertions.assertFalse(vehicleDTO instanceof CarDTO);
    Assertions.assertTrue(vehicleDTO instanceof VehicleDTO);

    VehicleDTO carDTO = carMapper.carToDTO(car);
    Assertions.assertTrue(carDTO instanceof CarDTO);
}

So, we can see that the base mapper isn’t capable of identifying the provided POJO object as an instance of Car. Furthermore, it isn’t able to dynamically pick the relevant subclass mapper, CarMapper, as well. So, the base mapper is only capable of mapping to the VehicleDTO object, regardless of the provided subclass instance.

4. MapStruct Inheritance With Instance Checks

The first approach is to instruct Mapstruct to generate the mapper method for each Vehicle type. We can then implement the generic conversion method for the base class by invoking the appropriate converter method for each subclass with instance checks using the Java instanceof operator:

@Mapper()
public interface VehicleMapperByInstanceChecks {
    CarDTO map(Car car);
    BusDTO map(Bus bus);

    default VehicleDTO mapToVehicleDTO(Vehicle vehicle) {
        if (vehicle instanceof Bus) {
            return map((Bus) vehicle);
        } else if (vehicle instanceof Car) {
            return map((Car) vehicle);
        } else {
            return null;
        }
    }
}

After the successful generation of the implementation class,  let’s verify the mapping for each subclass type by using the generic method:

@Test
void whenVehicleTypeIsCar_thenMappedToCarDTOCorrectly() {
    Car car = getCarInstance();

    VehicleDTO vehicleDTO = vehicleMapper.mapToVehicleDTO(car);
    Assertions.assertTrue(vehicleDTO instanceof CarDTO);
    Assertions.assertEquals(car.getTires(), ((CarDTO) vehicleDTO).getTires());
    Assertions.assertEquals(car.getSpeed(), vehicleDTO.getSpeed());
    Assertions.assertEquals(car.getColor(), vehicleDTO.getColor());
}

@Test
void whenVehicleTypeIsBus_thenMappedToBusDTOCorrectly() {
    Bus bus = getBusInstance();

    VehicleDTO vehicleDTO = vehicleMapper.mapToVehicleDTO(bus);
    Assertions.assertTrue(vehicleDTO instanceof BusDTO);
    Assertions.assertEquals(bus.getCapacity(), ((BusDTO) vehicleDTO).getCapacity());
    Assertions.assertEquals(bus.getSpeed(), vehicleDTO.getSpeed());
    Assertions.assertEquals(bus.getColor(), vehicleDTO.getColor());
}

We can use this approach to deal with any depth of inheritance. We just need to provide one mapping method for each hierarchy level using instance checks.

5. MapStruct Inheritance With Visitor Pattern

The second approach is to use the Visitor pattern. We can skip doing instance checks when we use the visitor pattern approach because Java uses polymorphism to determine exactly which method to invoke at runtime.

5.1. Applying the Visitor Pattern

We start by defining the abstract method accept() in our abstract class Vehicle to welcome any Visitor object:

public abstract class Vehicle {
    public abstract VehicleDTO accept(Visitor visitor);
}
public interface Visitor {
    VehicleDTO visit(Car car);
    VehicleDTO visit(Bus bus);
}

Now, we need to implement the accept() method for each Vehicle type:

public class Bus extends Vehicle {
    @Override
    VehicleDTO accept(Visitor visitor) {
        return visitor.visit(this);
    }
}

public class Car extends Vehicle {
    @Override
    VehicleDTO accept(Visitor visitor) {
        return visitor.visit(this);
    }
}

Finally, we can implement the mapper by implementing the Visitor interface:

@Mapper()
public abstract class VehicleMapperByVisitorPattern implements Visitor {
    public VehicleDTO mapToVehicleDTO(Vehicle vehicle) {
        return vehicle.accept(this);
    }

    @Override
    public VehicleDTO visit(Car car) {
        return map(car);
    }

    @Override
    public VehicleDTO visit(Bus bus) {
        return map(bus);
    }

    abstract CarDTO map(Car car);
    abstract BusDTO map(Bus bus);
}

Visitor pattern methodology is more highly optimized than the instance checks approach because, when the depth is high, there is no need to check all subclasses, which takes more time in mapping.

5.2. Testing the Visitor Pattern

After the successful generation of the implementation class,  let’s verify the mapping of each Vehicle type with the mapper implemented using the Visitor interface:

@Test
void whenVehicleTypeIsCar_thenMappedToCarDTOCorrectly() {
    Car car = getCarInstance();

    VehicleDTO vehicleDTO = vehicleMapper.mapToVehicleDTO(car);
    Assertions.assertTrue(vehicleDTO instanceof CarDTO);
    Assertions.assertEquals(car.getTires(), ((CarDTO) vehicleDTO).getTires());
    Assertions.assertEquals(car.getSpeed(), vehicleDTO.getSpeed());
    Assertions.assertEquals(car.getColor(), vehicleDTO.getColor());
}

@Test
void whenVehicleTypeIsBus_thenMappedToBusDTOCorrectly() {
    Bus bus = getBusInstance();

    VehicleDTO vehicleDTO = vehicleMapper.mapToVehicleDTO(bus);
    Assertions.assertTrue(vehicleDTO instanceof BusDTO);
    Assertions.assertEquals(bus.getCapacity(), ((BusDTO) vehicleDTO).getCapacity());
    Assertions.assertEquals(bus.getSpeed(), vehicleDTO.getSpeed());
    Assertions.assertEquals(bus.getColor(), vehicleDTO.getColor());
}

6. Mapstruct Inheritance With @SubclassMapping

As we previously mentioned, Mapstruct 1.5.0 introduced the @SubclassMapping annotation. This allows us to configure the mappings to handle the hierarchy of the source type. The source() function defines the subclass to be mapped, whereas target() specifies the subclass to map to:

public @interface SubclassMapping {
    Class<?> source();
    Class<?> target();
    // other methods
}

6.1. Applying the Annotation

Let’s apply the @SubclassMapping annotation to achieve inheritance in our Vehicle hierarchy:

@Mapper()
public interface VehicleMapperBySubclassMapping {
    @SubclassMapping(source = Car.class, target = CarDTO.class)
    @SubclassMapping(source = Bus.class, target = BusDTO.class)
    VehicleDTO mapToVehicleDTO(Vehicle vehicle);
}

To understand how @SubclassMapping works internally, let’s go through the implementation class generated under /target/generated-sources/annotations/:

@Generated
public class VehicleMapperBySubclassMappingImpl implements VehicleMapperBySubclassMapping {
    @Override
    public VehicleDTO mapToVehicleDTO(Vehicle vehicle) {
        if (vehicle == null) {
            return null;
        }

        if (vehicle instanceof Car) {
            return carToCarDTO((Car) vehicle);
        } else if (vehicle instanceof Bus) {
            return busToBusDTO((Bus) vehicle);
        } else {
            VehicleDTO vehicleDTO = new VehicleDTO();

            vehicleDTO.setColor(vehicle.getColor());
            vehicleDTO.setSpeed(vehicle.getSpeed());

            return vehicleDTO;
        }
    }
}

Based on the implementation, we can notice that internally, Mapstruct uses instance checks to pick the subclass dynamically. So, we can deduce that for every layer in the hierarchy, we need to define a @SubclassMapping.

6.2. Testing the Annotation

We can now verify the mapping of each Vehicle type by using the Mapstruct mapper implemented with the @SubclassMapping annotation:

@Test
void whenVehicleTypeIsCar_thenMappedToCarDTOCorrectly() {
    Car car = getCarInstance();

    VehicleDTO vehicleDTO = vehicleMapper.mapToVehicleDTO(car);
    Assertions.assertTrue(vehicleDTO instanceof CarDTO);
    Assertions.assertEquals(car.getTires(), ((CarDTO) vehicleDTO).getTires());
    Assertions.assertEquals(car.getSpeed(), vehicleDTO.getSpeed());
    Assertions.assertEquals(car.getColor(), vehicleDTO.getColor());
}

@Test
void whenVehicleTypeIsBus_thenMappedToBusDTOCorrectly() {
    Bus bus = getBusInstance();

    VehicleDTO vehicleDTO = vehicleMapper.mapToVehicleDTO(bus);
    Assertions.assertTrue(vehicleDTO instanceof BusDTO);
    Assertions.assertEquals(bus.getCapacity(), ((BusDTO) vehicleDTO).getCapacity());
    Assertions.assertEquals(bus.getSpeed(), vehicleDTO.getSpeed());
    Assertions.assertEquals(bus.getColor(), vehicleDTO.getColor());
}

7. Conclusion

In this article, we’ve discussed how to write Mapstruct mappers with object classes that are inherited.

The first approach we discussed used instanceof checks, while the second used the prominent Visitor pattern. However, things can be made simpler by using the Mapstruct feature @SubclassMapping.

As always, the full source code 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)
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments