MapStruct With Inheritance Examples
1. Introduction
MapStruct is an open-source, compile-time code generator, and annotation processor. It simplifies the implementation of mappings between different Java bean types based on a convention over configuration approach. In this tutorial, we’ll demonstrate Mapstruct inheritance problem along with the four solutions:
- Address Mapstruct inheritance problem via instance-check.
- Address Mapstruct inheritance problem via the
Visitordesign pattern. - Address Mapstruct inheritance problem via the higher-order function along with instance-check.
- Address Mapstruct inheritance problem via the
@SubclassMappingannotation introduced inMapstruct1.5.0.
2. Setup
In this step, I will create a gradle project with MapStruct, Lombok, and Junit libraries.
2.1 Gradle Build Script
The build.gradle includes MapStruct, Lombok, and Junit libraries.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.0'
id 'io.spring.dependency-management' version '1.1.5'
id 'com.diffplug.eclipse.apt' version '3.37.2'
}
group = 'com.zheng.demo.sbtest'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
2.2 DemoApplication
The following class is generated by the spring initiaizr.
DemoApplication.java
package com.zheng.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2.3 DemoApplicationTests
The following test class is generated by the spring initiaizr.
DemoApplicationTests.java
package com.zheng.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
}
}
Run the test and confirm that the spring context is loaded without any error.
3. Java Beans
In this step, I will create Java Beans with both super and child classes. The last four Java Beans: Food, Wine, FoodDto, and WineDto are used with the Visitor solution outlined at step 5.3. Use the Lombok library to reduce the boilerplate code.
Vehicle– superclass to bothCarandTrain.Car– child class extended fromVehicle.Train– child class extended fromVehicle.VehicleDto– superclass to bothCarDtoandTrainDto.CarDto– child class extended fromVehicleDto.TrainDto– child class extended fromVehicleDto.Food– superclass toWine.Wine– child class extended fromFood.FoodDto– superclass toWineDto.WineDto– child class extended fromFoodDto.
As you saw in the following image, The mapper interfaces created at step 4 map Java Beans. For example, Vehicle to VehicleDto, Car to CarDto, Train to TrainDto, and vice versa.
3.1 Vehicle Base Class
In this step, I will create a base Vehicle class.
Vehicle.java
package com.zheng.demo.data;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@NoArgsConstructor
public class Vehicle {
private String color;
private String speed;
}Note: use annotations from Lombok to reduce boilerplate code.
3.2 Car Child Class
In this step, I will create a child Car class extended from Vehicle.
Car.java
package com.zheng.demo.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Car extends Vehicle {
private String make;
private int numberOfSeats;
private String type;
}
3.3 Train Child Class
In this step, I will create a child Train class from Vehicle.
Train.java
package com.zheng.demo.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Train extends Vehicle {
private int numberOfKarts;
}
3.4 VehicleDto Base Class
In this step, I will create a base VehicleDto class which is the counterpart of the Vehicle class.
VehicleDto.java
package com.zheng.demo.data;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@NoArgsConstructor
public class VehicleDto {
private String color;
private String speed;
}Note: this is a base class and has two children: CarDto and TrainDto.
3.5 CarDto Child Class
In this step, I will create a child CarDto class extended from VehicleDto.
CarDto.java
package com.zheng.demo.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CarDto extends VehicleDto {
private String make;
private int seatCount;
private String type;
}
3.6 TrainDto Child Class
In this step, I will create a child TrainDto class from VehicleDto.
TrainDto.java
package com.zheng.demo.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class TrainDto extends VehicleDto {
private int numberOfKarts;
}
3.7 Food Base Class
In this step, I will create a base Food class which adapts the Visitor design pattern, so it implements the Visitable interface.
Food.java
package com.zheng.demo.data;
import org.zheng.demo.visitor.MVisitor;
import org.zheng.demo.visitor.Visitable;
import lombok.Data;
@Data
public class Food implements Visitable {
protected float price;
protected float taxRate;
@Override
public FoodDto accept(MVisitor visitor) {
return visitor.visit(this);
}
}
- Line 9: implements the
Visitableinterface with theacceptmethod according to theVisitorpattern outlined at step 5.3. - Line 15: the
acceptmethod is just calling thevisitor.visit(this)based on thevisitorpattern.
3.8 Wine Child Class
In this step, I will create a child Wine class extended from Food.
Wine.java
package com.zheng.demo.data;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class Wine extends Food {
private float requiredAge;
}
3.9 FoodDto Base Class
In this step, I will create a base FoodDto class.
FoodDto.java
package com.zheng.demo.data;
import lombok.Data;
@Data
public class FoodDto {
protected float price;
protected float taxRate;
}
3.10 WineDto Child Class
In this step, I will create a child WineDto class extended from FoodDto.
WineDto.java
package com.zheng.demo.data;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class WineDto extends FoodDto {
private float requiredAge;
}
4. Mapper Interfaces
In this step, I will create mapper interfaces with the @Mapper annotation and let MapStruct generate the implementation classes. I will show that the default mapper implementation class does not support the inheritance at step 4.1.
4.1 Vehicle Mapper
In this step, I will create a VehicleMapper.java interface to map the Vehicle and VehicleDto.
VehicleMapper.java
package com.zheng.demo.mapper;
import org.mapstruct.Mapper;
import com.zheng.demo.data.Vehicle;
import com.zheng.demo.data.VehicleDto;
@Mapper(uses = { CarMapper.class, TrainMapper.class })
public interface VehicleMapper {
Vehicle dtoToVehicle(VehicleDto vehicleDto);
VehicleDto vehicleToDto(Vehicle vehicle);
}
Note: the @Mapper annotation generates the implementation class via the MapStruct processor.
Create a unit test VehicleMapperTest.java class to verify the generated mapper implementation does not support the inheritance as expected. This issue will be addressed at step 5.
VehicleMapperTest.java
package com.zheng.demo.mapper;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import com.zheng.demo.data.Car;
import com.zheng.demo.data.CarDto;
import com.zheng.demo.data.Vehicle;
import com.zheng.demo.data.VehicleDto;
class VehicleMapperTest {
private VehicleMapper mapper = new VehicleMapperImpl();
@Test
void test_issue_car_not_not_to_carDto_on_vehicle_mapping() {
Vehicle vh = new Vehicle();
vh.setColor("white");
vh.setSpeed("50mph");
VehicleDto vhDto = mapper.vehicleToDto(vh);
assertThat(vhDto).isNotNull();
assertThat(vhDto.getColor()).isEqualTo(vh.getColor());
assertThat(vhDto.getSpeed()).isEqualTo(vh.getSpeed());
Vehicle vh2 = mapper.dtoToVehicle(vhDto);
assertThat(vh2).isNotNull();
assertThat(vh2.getColor()).isEqualTo(vhDto.getColor());
assertThat(vh2.getSpeed()).isEqualTo(vhDto.getSpeed());
Car car = new Car("Morris", 5, "SEDAN");
VehicleDto vehicleDto = mapper.vehicleToDto(car);
assertTrue(vehicleDto instanceof VehicleDto);
// This is the issue as the Car did not map to CarDto @SubMapping fix it
assertFalse(vehicleDto instanceof CarDto);
}
}
- Line 43: the mapped data type is not
CarDtowhen the input argument type isCar. This is theMapStructInheritance problem.
4.2 Car Mapper
In this step, I will create a CarMapper.java interface to map the Car and CarDto.
CarMapper.java
package com.zheng.demo.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import com.zheng.demo.data.Car;
import com.zheng.demo.data.CarDto;
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(target = "numberOfSeats", source = "seatCount")
Car carDtoToCar(CarDto carDto);
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDto carToCarDto(Car car);
}
- Line 10:
@Mapperannotation creates the implementation class. - Line 14,17: defines the field mapping for both
sourceandtargetfields when the mapping beans don’t have the same field name.
Create a unit test CarMapperTest.java and verify the generated implementation class works as expected.
CarMapperTest.java
package com.zheng.demo.mapper;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import com.zheng.demo.data.Car;
import com.zheng.demo.data.CarDto;
class CarMapperTest {
private Car dummyCar() {
Car car = new Car("Morris", 5, "SEDAN");
car.setColor("white");
car.setSpeed("50mph");
return car;
}
@Test
void test_CarMapper() {
// given
Car car = dummyCar();
// when
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
// then
assertThat(carDto).isNotNull();
assertThat(carDto.getMake()).isEqualTo(car.getMake());
assertThat(carDto.getSeatCount()).isEqualTo(car.getNumberOfSeats());
assertThat(carDto.getType()).isEqualTo(car.getType());
assertThat(carDto.getColor()).isEqualTo(car.getColor());
assertThat(carDto.getSpeed()).isEqualTo(car.getSpeed());
Car car2 = CarMapper.INSTANCE.carDtoToCar(carDto);
assertThat(car2).isNotNull();
assertThat(car2.getMake()).isEqualTo(carDto.getMake());
assertThat(car2.getNumberOfSeats()).isEqualTo(carDto.getSeatCount());
assertThat(car2.getType()).isEqualTo(carDto.getType());
assertThat(car2.getColor()).isEqualTo(carDto.getColor());
assertThat(car2.getSpeed()).isEqualTo(carDto.getSpeed());
}
}
Run the unit tests and it passed. The child mapper maps fields correctly.
4.3 Train Mapper
In this step, I will create a TrainMapper.java interface to map the Train and TrainDto.
TrainMapper.java
package com.zheng.demo.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import com.zheng.demo.data.Train;
import com.zheng.demo.data.TrainDto;
@Mapper
public interface TrainMapper {
TrainMapper INSTANCE = Mappers.getMapper(TrainMapper.class);
Train trainDtoToTrain(TrainDto tnDto);
TrainDto trainToTrainDto(Train car);
}
Create a TrainMapperTest.java class and verify the generated implementation class works as expected.
TrainMapperTest.java
package com.zheng.demo.mapper;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import com.zheng.demo.data.Train;
import com.zheng.demo.data.TrainDto;
class TrainMapperTest {
private TrainMapper mapper = new TrainMapperImpl();
private Train dummyTrain() {
return Train.builder().speed("50mph").color("white").numberOfKarts(5).build();
}
@Test
void test_TrainMapper() {
// given
Train train = dummyTrain();
// when
TrainDto trainDto = mapper.trainToTrainDto(train);
// then
assertThat(trainDto).isNotNull();
assertThat(trainDto.getNumberOfKarts()).isEqualTo(train.getNumberOfKarts());
assertThat(trainDto.getColor()).isEqualTo(train.getColor());
assertThat(trainDto.getSpeed()).isEqualTo(train.getSpeed());
Train train2 = mapper.trainDtoToTrain(trainDto);
assertThat(train2).isNotNull();
assertThat(train2.getNumberOfKarts()).isEqualTo(trainDto.getNumberOfKarts());
assertThat(train2.getColor()).isEqualTo(trainDto.getColor());
assertThat(train2.getSpeed()).isEqualTo(trainDto.getSpeed());
}
}
Run the unit tests and it should pass.
4.4 Food Mapper
In this step, I will create a FoodMapper interface which maps the Food and FoodDto. It will be used at step 5.3.
FoodMapper.java
package com.zheng.demo.mapper;
import org.mapstruct.Mapper;
import com.zheng.demo.data.Food;
import com.zheng.demo.data.FoodDto;
@Mapper
public interface FoodMapper {
FoodDto map(Food food);
}
4.5 Wine Mapper
In this step, I will create a WineMapper interface to map the Wine to WineDto and vice versa. It will be used at step 5.3.
WineMapper.java
package com.zheng.demo.mapper;
import org.mapstruct.Mapper;
import com.zheng.demo.data.Wine;
import com.zheng.demo.data.WineDto;
@Mapper
public interface WineMapper {
WineDto map(Wine wine);
}
5. Address Mapstruct Inheritance Problem
5.1 Via SubMapping
In this step, I will create a VehicleMapper_SubclassMapping.java interface to map the Vehicle to VehicleDto and vice versa.
VehicleMapper_SubclassMapping.java
package com.zheng.demo.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.SubclassMapping;
import com.zheng.demo.data.Car;
import com.zheng.demo.data.CarDto;
import com.zheng.demo.data.Train;
import com.zheng.demo.data.TrainDto;
import com.zheng.demo.data.Vehicle;
import com.zheng.demo.data.VehicleDto;
@Mapper(uses = { CarMapper.class, TrainMapper.class })
public interface VehicleMapper_SubclassMapping {
@SubclassMapping(source = CarDto.class, target = Car.class)
@SubclassMapping(source = TrainDto.class, target = Train.class)
Vehicle dtoToVehicle(VehicleDto vehicleDto);
@SubclassMapping(source = Car.class, target = CarDto.class)
@SubclassMapping(source = Train.class, target = TrainDto.class)
VehicleDto vehicleToDto(Vehicle vehicle);
}
- Line 15,16,19,20: add
@SubclassMappingto define thesourceandtargetclass types.
Create a unit test VehicleMapper_SubclassMappingTest.java and verify the generated implementation class works as expected for inheritance.
VehicleMapper_SubclassMappingTest.java
package com.zheng.demo.mapper;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import com.zheng.demo.data.Car;
import com.zheng.demo.data.CarDto;
import com.zheng.demo.data.Train;
import com.zheng.demo.data.TrainDto;
import com.zheng.demo.data.Vehicle;
import com.zheng.demo.data.VehicleDto;
class VehicleMapper_SubclassMappingTest {
private VehicleMapper_SubclassMapping mapper = new VehicleMapper_SubclassMappingImpl();
@Test
void shouldMapParentAndChildFields_car() {
CarDto carDto = CarDto.builder().seatCount(6).color("white").make("Chevrolet").type("Malibu").speed("50mph")
.build();
Car car = (Car) mapper.dtoToVehicle(carDto);
assertThat(car).isNotNull();
assertThat(car.getMake()).isEqualTo(carDto.getMake());
assertThat(car.getNumberOfSeats()).isEqualTo(carDto.getSeatCount());
assertThat(car.getType()).isEqualTo(carDto.getType());
assertThat(car.getColor()).isEqualTo(carDto.getColor());
assertThat(car.getSpeed()).isEqualTo(carDto.getSpeed());
// map back to CarDto
CarDto carDto2 = (CarDto) mapper.vehicleToDto(car);
assertThat(carDto2).isNotNull();
assertThat(carDto2.getMake()).isEqualTo(car.getMake());
assertThat(carDto2.getSeatCount()).isEqualTo(car.getNumberOfSeats());
assertThat(carDto2.getType()).isEqualTo(car.getType());
// super class
assertThat(carDto2.getColor()).isEqualTo(car.getColor());
assertThat(carDto2.getSpeed()).isEqualTo(car.getSpeed());
}
@Test
void shouldMapParentAndChildFields_train() {
TrainDto trainDto = TrainDto.builder().numberOfKarts(6).color("white").speed("50mph").build();
Vehicle train = mapper.dtoToVehicle(trainDto);
assertThat(train).isNotNull();
assertTrue(train instanceof Train);
assertThat(train.getColor()).isEqualTo(trainDto.getColor());
assertThat(train.getSpeed()).isEqualTo(trainDto.getSpeed());
// map back to CarDto
VehicleDto trainDto2 = mapper.vehicleToDto(train);
assertTrue(trainDto2 instanceof TrainDto);
assertThat(trainDto2).isNotNull();
assertThat(trainDto2.getColor()).isEqualTo(train.getColor());
assertThat(trainDto2.getSpeed()).isEqualTo(train.getSpeed());
}
}
- Note: this is the cleanest way to address the
MapStructInheritance issue. - Line 53, 60: the Inheritance issue is fixed as the mapped data object is no longer the superclass, it is the child class which matches the input class type.
Run the unit tests and it should pass.
5.2 Via Instance Check
In this step, I will create a VehicleMapper_InstanceCheck.java
interface which has three mapping methods. The two children mapper can use the generate code directly. But the base Vehicle to VehicelDto mapping needs check the input argument’s type, then use the child’s map method accordingly.
VehicleMapper_InstanceCheck
package com.zheng.demo.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import com.zheng.demo.data.Car;
import com.zheng.demo.data.CarDto;
import com.zheng.demo.data.Train;
import com.zheng.demo.data.TrainDto;
import com.zheng.demo.data.Vehicle;
import com.zheng.demo.data.VehicleDto;
@Mapper()
public interface VehicleMapper_InstanceCheck {
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDto map(Car car);
TrainDto map(Train train);
default VehicleDto mapToVehicleDto(Vehicle vehicle) {
if (vehicle instanceof Train) {
return map((Train) vehicle);
} else if (vehicle instanceof Car) {
return map((Car) vehicle);
} else {
return null;
}
}
}- Line 13:
@MapperfromMapStruct. - Line 19,20,22: the default interface method which checks the input class type and then calls its mapper method to address the
MapStructinheritance problem.
Create a unit test VehicleMapper_SubclassMappingTest.java and verify the generated implementation class works as expected for inheritance.
VehicleMapper_InstanceCheckTest.java
package com.zheng.demo.mapper;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import com.zheng.demo.data.Car;
import com.zheng.demo.data.CarDto;
import com.zheng.demo.data.VehicleDto;
class VehicleMapper_InstanceCheckTest {
private VehicleMapper_InstanceCheck mapper = new VehicleMapper_InstanceCheckImpl();
@Test
void shouldMapParentAndChildFields() {
Car car = Car.builder().numberOfSeats(6).color("white").make("Chevrolet").type("Malibu").speed("50mph").build();
VehicleDto vehicleDto = mapper.mapToVehicleDto(car);
assertThat(vehicleDto).isNotNull();
//addressed the inheritance
assertTrue(vehicleDto instanceof CarDto);
assertThat(car.getMake()).isEqualTo(((CarDto) vehicleDto).getMake());
assertThat(car.getNumberOfSeats()).isEqualTo(((CarDto) vehicleDto).getSeatCount());
assertThat(car.getType()).isEqualTo(((CarDto) vehicleDto).getType());
assertThat(car.getColor()).isEqualTo(vehicleDto.getColor());
assertThat(car.getSpeed()).isEqualTo(vehicleDto.getSpeed());
}
}
- Line 24: the
MapStructinhertiance issue is resolved as the mapped class type is not the superclass as outlined at step 4.1.
5.3 Via Visitor Design Pattern
The Visitor design pattern is a behavioral design pattern that separates algorithms from the objects on which they operate. It requires both Visitor and Visitable interfaces. The detailed operations for each class type need to implement the Visitor interface and each data class type needs to implement the Visitable interface. For a clear demonstration purpose, I will use the four data beans: Food, Wine, FoodDto, and WineDto created at step 3 and two mapper interfaces: FoodMapper and WineMapper created at step 4.
Food– superclass toWine.Wine– child class extended fromFood.FoodDto– superclass toWineDto.WineDto– child class extended fromFoodDto.FoodMapper– a food mapper interface.WineMapper– a wine mapper interface.
Create a MVisitore.java interface with two map methods. One for each class type.
MVisitor.java
package org.zheng.demo.visitor;
import com.zheng.demo.data.Food;
import com.zheng.demo.data.FoodDto;
import com.zheng.demo.data.Wine;
public interface MVisitor {
FoodDto visit(Food food);
FoodDto visit(Wine wine);
}
- Line 8, 10: for each class type, it should have an overloaded
visitmethod based on thevisitorpattern..
Create a Visitable.java interface with just the accept method. As you see at step 3, both Food and Wine implement it.
Visitable.java
package org.zheng.demo.visitor;
import com.zheng.demo.data.FoodDto;
public interface Visitable {
FoodDto accept(MVisitor visitor);
}
Create a MapVisitor.java which implements the MVisitore interface to address the inheritance issue outlined at step 4.1.
MapVisitor.java
package org.zheng.demo.visitor;
import com.zheng.demo.data.Food;
import com.zheng.demo.data.FoodDto;
import com.zheng.demo.data.Wine;
import com.zheng.demo.mapper.FoodMapper;
import com.zheng.demo.mapper.FoodMapperImpl;
import com.zheng.demo.mapper.WineMapper;
import com.zheng.demo.mapper.WineMapperImpl;
public class MapVisitor implements MVisitor {
@Override
public FoodDto visit(Food food) {
FoodMapper foodMapper = new FoodMapperImpl();
return foodMapper.map(food);
}
@Override
public FoodDto visit(Wine wine) {
WineMapper wineMapper = new WineMapperImpl();
return wineMapper.map(wine);
}
}
Note: the Visitor pattern requires each class to have its overloaded visit method.
Create a unit test VisitorMapTest.java and verify the mapper works as expected regardless of the superclass or child class.
VisitorMapTest.java
package org.zheng.demo.visitor;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import com.zheng.demo.data.Food;
import com.zheng.demo.data.FoodDto;
import com.zheng.demo.data.Wine;
import com.zheng.demo.data.WineDto;
class VisitorMapTest {
MapVisitor map = new MapVisitor();
@Test
void test_map_via_visitor() {
Food food = new Food();
food.setPrice(100);
FoodDto mapped = map.visit(food);
assertTrue(mapped instanceof FoodDto);
Wine wine = new Wine();
wine.setPrice(100);
FoodDto mapped2 = map.visit(wine);
assertTrue(mapped2 instanceof WineDto);
}
}
- Line 28: the mapped class type is correct. it addressed the problem outlined at step 4.1.
Run the unit test and it passed as expected. Line 28 confirmed that the MapStruct inheritance problem is fixed.
5.4 Via Higher-Order Function
Java 8 introduced a functional program, so we can address the MapStruct inheritance problem with a higher-order function instead of the visitor pattern as it requires some boilerplate code (Visitor and Visitable Interfaces). In this step, I will create a unit test TestHigherOrderFunction.java which contains three higher-order functions, one for each data type.
TestHigherOrderFunction.java
package com.zheng.demo.mapper;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
import com.zheng.demo.data.Car;
import com.zheng.demo.data.CarDto;
import com.zheng.demo.data.Train;
import com.zheng.demo.data.Vehicle;
import com.zheng.demo.data.VehicleDto;
class TestHigherOrderFunction {
Function<CarDto, Car> carDtoToCarMap = (c -> {
return Mappers.getMapper(CarMapper.class).carDtoToCar(c);
});
Function<Car, CarDto> carToCarDtoMap = (c -> {
return Mappers.getMapper(CarMapper.class).carToCarDto(c);
});
Function<Vehicle, VehicleDto> vehicleDtoToCarMap = (c -> {
if (c instanceof Car) {
return (VehicleDto) Mappers.getMapper(CarMapper.class).carToCarDto((Car) c);
} else if (c instanceof Train) {
return (VehicleDto) Mappers.getMapper(TrainMapper.class).trainToTrainDto((Train) c);
} else {
return null;
}
});
private Car applyCarDtoToCarMap(CarDto carDto, Function<CarDto, Car> function) {
return function.apply(carDto);
}
private CarDto applyCarToCarDtoMap(Car car, Function<Car, CarDto> function) {
return function.apply(car);
}
private VehicleDto applyToDtoMap(Vehicle car, Function<Vehicle, VehicleDto> function) {
return function.apply(car);
}
@Test
void test_map_via_high_order_functions() {
Car car = new Car("Morris", 5, "SEDAN");
car.setColor("white");
car.setSpeed("50mph");
CarDto cd = applyCarToCarDtoMap(car, carToCarDtoMap);
assertTrue(cd instanceof CarDto);
Car car2 = applyCarDtoToCarMap(cd, carDtoToCarMap);
assertTrue(car2 instanceof Car);
VehicleDto carV = applyToDtoMap(car, vehicleDtoToCarMap);
assertTrue(carV instanceof CarDto);
}
}
- Line 18, 22: create the mapper functions for each child type.
- Line 26: create the superclass mapper function which checks the input’s type and then invokes its map method.
- Line 36, 40, 44: create higher-order function to apply the mapping logic.
- Line 54, 57, 60: apply the higher-order function. As you saw here, the logic is very similar to the
Visitorpattern but with the functional approach. This approach no longer needs theVisitorandVisitableInterfaces.
Run the tests and all passed.
6. Conclusion
In this example, I created several Java Beans and mapper interfaces. I outlined the Mapstruct inheritance problem and showed four ways to solve it.
- Added the
@Submappingannotation to the superclass mapper interface. This is the best solution and requires the newer version ofMapStruct. - Added a default method in the superclass’s
mapperinterface and implemented it by examining the argument class type and then calling its corresponding mapper. This requires Java version 8+. - Adapted the
Visitordesign pattern for each class type. - Adapted the higher-order function. This also requires Java 8+. The solution is similar to the
Visitorpattern but functional.
7. Download
This was an example of a gradle project which included MapStruct Inheritance problem and its solutions.
You can download the full source code of this example here: MapStruct With Inheritance Examples


