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
Visitor
design pattern. - Address Mapstruct inheritance problem via the higher-order function along with instance-check.
- Address Mapstruct inheritance problem via the
@SubclassMapping
annotation introduced inMapstruct
1.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 bothCar
andTrain
.Car
– child class extended fromVehicle
.Train
– child class extended fromVehicle
.VehicleDto
– superclass to bothCarDto
andTrainDto
.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
Visitable
interface with theaccept
method according to theVisitor
pattern outlined at step 5.3. - Line 15: the
accept
method is just calling thevisitor.visit(this)
based on thevisitor
pattern.
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
CarDto
when the input argument type isCar
. This is theMapStruct
Inheritance 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:
@Mapper
annotation creates the implementation class. - Line 14,17: defines the field mapping for both
source
andtarget
fields 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
@SubclassMapping
to define thesource
andtarget
class 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
MapStruct
Inheritance 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:
@Mapper
fromMapStruct
. - Line 19,20,22: the default interface method which checks the input class type and then calls its mapper method to address the
MapStruct
inheritance 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
MapStruct
inhertiance 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
visit
method based on thevisitor
pattern..
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
Visitor
pattern but with the functional approach. This approach no longer needs theVisitor
andVisitable
Interfaces.
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
@Submapping
annotation to the superclass mapper interface. This is the best solution and requires the newer version ofMapStruct
. - Added a default method in the superclass’s
mapper
interface and implemented it by examining the argument class type and then calling its corresponding mapper. This requires Java version 8+. - Adapted the
Visitor
design pattern for each class type. - Adapted the higher-order function. This also requires Java 8+. The solution is similar to the
Visitor
pattern 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