Core Java

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 in Mapstruct 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 both Car and Train.
  • Car – child class extended from Vehicle.
  • Train – child class extended from Vehicle.
  • VehicleDto – superclass to both CarDto and TrainDto.
  • CarDto – child class extended from VehicleDto.
  • TrainDto – child class extended from VehicleDto.
  • Food – superclass to Wine.
  • Wine – child class extended from Food.
  • FoodDto – superclass to WineDto.
  • WineDto – child class extended from FoodDto.

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.

Figure 1. Java Beans

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 the accept method according to the Visitor pattern outlined at step 5.3.
  • Line 15: the accept method is just calling the visitor.visit(this) based on the visitor 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 is Car. This is the MapStruct 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 and target 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 the source and target 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 from MapStruct.
  • 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 to Wine.
  • Wine – child class extended from Food.
  • FoodDto – superclass to WineDto.
  • WineDto – child class extended from FoodDto.
  • 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 the visitor 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 the Visitor and Visitable 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 of MapStruct.
  • 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.

Download
You can download the full source code of this example here: MapStruct With Inheritance Examples

Mary Zheng

Mary graduated from the Mechanical Engineering department at ShangHai JiaoTong University. She also holds a Master degree in Computer Science from Webster University. During her studies she has been involved with a large number of projects ranging from programming and software engineering. She worked as a lead Software Engineer in the telecommunications sector where she led and worked with others to design, implement, and monitor the software solution.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button