MapStruct's @SubclassMapping elegantly solves polymorphic object mapping in inheritance scenarios, generating runtime type-safe conversions from parent to child classes. This annotation shines when mapping abstract base classes or interfaces with concrete subclasses, avoiding manual instanceof checks while ensuring correct target instantiation.
Core Problem It Solves
Without @SubclassMapping, MapStruct treats inheritance hierarchies naively—mapping a Vehicle source to VehicleDTO target ignores subclass details, losing Car-specific doors or Bus-specific seats. The annotation configures explicit source→target subclass pairs on parent-level mapper methods, generating dispatch logic like if (source instanceof Car) return mapCar((Car) source). Use it precisely when source/target hierarchies diverge or when abstract targets need concrete delegation.
Complete Compilable Example
// Source hierarchy
abstract class Vehicle {
private String brand;
public String getBrand() { return brand; }
public void setBrand(String brand) { this.brand = brand; }
}
class Car extends Vehicle {
private int doors;
public int getDoors() { return doors; }
public void setDoors(int doors) { this.doors = doors; }
}
class Bus extends Vehicle {
private int seats;
public int getSeats() { return seats; }
public void setSeats(int seats) { this.seats = seats; }
}
// Target hierarchy (mirrors structure)
abstract class VehicleDTO {
private String brand;
public String getBrand() { return brand; }
public void setBrand(String brand) { this.brand = brand; }
}
class CarDTO extends VehicleDTO {
private int doors;
public int getDoors() { return doors; }
public void setDoors(int doors) { this.doors = doors; }
}
class BusDTO extends VehicleDTO {
private int seats;
public int getSeats() { return seats; }
public void setSeats(int seats) { this.seats = seats; }
}
Mapper with @SubclassMapping
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.SubclassMapping;
@Mapper(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION)
interface VehicleMapper {
@SubclassMapping(source = Car.class, target = CarDTO.class)
@SubclassMapping(source = Bus.class, target = BusDTO.class)
VehicleDTO toDTO(Vehicle vehicle);
@Mapping(target = "doors", source = "doors")
CarDTO toCarDTO(Car car);
@Mapping(target = "seats", source = "seats")
BusDTO toBusDTO(Bus bus);
}
Key Requirements: Define concrete subclass mappers (toCarDTO, toBusDTO) explicitly—MapStruct delegates to them. @SubclassMapping pairs source/target subclasses; repeat for each pair.
Generated Dispatch Logic
MapStruct produces clean instanceof dispatch:
public VehicleDTO toDTO(Vehicle vehicle) {
if ( vehicle == null ) {
return null;
}
if (vehicle instanceof Car) {
return toCarDTO( (Car) vehicle );
}
else if (vehicle instanceof Bus) {
return toBusDTO( (Bus) vehicle );
}
else {
throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for " + vehicle.getClass());
}
}
For abstract targets, it skips parent instantiation entirely. Configure subclassExhaustiveStrategy for custom fallbacks (e.g., default parent mapping).
Usage and Verification
import org.mapstruct.factory.Mappers;
public class Demo {
public static void main(String[] args) {
VehicleMapper mapper = Mappers.getMapper(VehicleMapper.class);
Car car = new Car(); car.setBrand("Toyota"); car.setDoors(4);
Bus bus = new Bus(); bus.setBrand("Volvo"); bus.setSeats(40);
VehicleDTO carDTO = mapper.toDTO(car); // instanceof CarDTO
VehicleDTO busDTO = mapper.toDTO(bus); // instanceof BusDTO
System.out.println(carDTO.getClass().getSimpleName() + ": " + ((CarDTO)carDTO).getDoors()); // CarDTO: 4
System.out.println(busDTO.getClass().getSimpleName() + ": " + ((BusDTO)busDTO).getSeats()); // BusDTO: 40
}
}
Runtime polymorphism works seamlessly—input Car yields CarDTO with preserved subclass state.
Leave a Reply