eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – Spring Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

Partner – Diagrid – NPI EA (cat= Testing)
announcement - icon

In distributed systems, managing multi-step processes (e.g., validating a driver, calculating fares, notifying users) can be difficult. We need to manage state, scattered retry logic, and maintain context when services fail.

Dapr Workflows solves this via Durable Execution which includes automatic state persistence, replaying workflows after failures and built-in resilience through retries, timeouts and error handling.

In this tutorial, we'll see how to orchestrate a multi-step flow for a ride-hailing application by integrating Dapr Workflows and Spring Boot:

>> Dapr Workflows With PubSub

Course – Spring Sale 2026 – NPI (cat=Baeldung)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

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 classlet’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 classlet’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.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – Spring Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

Course – Spring Sale 2026 – NPI (All)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

eBook Jackson – NPI EA – 3 (cat = Jackson)