Core Java

Handling Abstract Class Mapping in MapStruct

MapStruct is a powerful Java annotation-based code generation framework that simplifies object-to-object mapping. However, when working with abstract classes, direct instantiation is not possible. To resolve this, MapStruct provides multiple ways to supply concrete implementations using factory methods. Let us delve into understanding how mapstruct map abstract class scenarios work in real-world java applications.

1. What Are Abstract Classes in Java?

An abstract class in Java is a class that cannot be instantiated directly. It is designed to be inherited by child classes and may contain:

  • Abstract methods (without implementation)
  • Concrete methods (with implementation)
  • Fields and constructors

Since abstract classes cannot be created using new, frameworks like MapStruct need a concrete class to instantiate during mapping.

2. What Is MapStruct?

MapStruct is a compile-time code generator for mapping between Java objects. Instead of writing manual mapping logic, developers define mapper interfaces, and MapStruct generates the implementation automatically. It offers:

  • High performance (no reflection)
  • Type-safe mappings
  • Clean, maintainable code

3. Code Example

3.1 Maven Dependency (pom.xml)

This configuration adds the required MapStruct libraries to enable annotation-based code generation during compilation.

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>stable__jar__version</version>
    </dependency>

    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>stable__jar__version</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

This dependency setup ensures that MapStruct APIs are available at runtime while the annotation processor runs only during compilation to generate mapper implementations.

3.2 DTO Class

This class represents the source object that typically comes from an API request or user interface layer.

public class VehicleDTO {

    private String type;
    private String model;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }
}

This DTO holds the raw input values where the type field drives the selection of the concrete subclass and the model field is mapped directly to the target object.

3.3 Abstract Target Class

This abstract class acts as the target type that cannot be instantiated directly during mapping.

public abstract class Vehicle {

    protected String model;

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public abstract String getVehicleType();
}

The abstract class defines the common structure for all vehicle types while enforcing subclass-specific behavior through the abstract method getVehicleType().

3.4 Concrete Implementation

These concrete classes provide actual implementations for the abstract vehicle type.

public class Car extends Vehicle {

    @Override
    public String getVehicleType() {
        return "Car";
    }
}

public class Bike extends Vehicle {

    @Override
    public String getVehicleType() {
        return "Bike";
    }
}

The Car and Bike classes extend the abstract parent and return their respective vehicle types, enabling runtime polymorphism during mapping.

3.5 External Factory Class Approach

3.5.1 Factory Class

This factory is responsible for deciding which concrete implementation should be created at runtime.

public class VehicleFactory {

    public Vehicle createVehicle(VehicleDTO dto) {

        if ("car".equalsIgnoreCase(dto.getType())) {
            return new Car();
        } 
        else if ("bike".equalsIgnoreCase(dto.getType())) {
            return new Bike();
        }

        throw new IllegalArgumentException("Unknown vehicle type");
    }
}

The factory evaluates the type value from the DTO and dynamically returns either a Car or Bike instance before MapStruct applies field-level mapping.

3.5.2 Mapper Using External Factory

This mapper delegates object creation to the external factory before assigning values.

@Mapper(uses = VehicleFactory.class)
public interface VehicleMapperWithExternalFactory {

    VehicleMapperWithExternalFactory INSTANCE =
            Mappers.getMapper(VehicleMapperWithExternalFactory.class);

    @Mapping(target = "model", source = "model")
    Vehicle toVehicle(VehicleDTO dto);
}

Here MapStruct injects the external factory automatically and calls it to construct the correct subclass before mapping the model field.

3.6 Inline @ObjectFactory Approach

This approach embeds the factory logic directly inside the mapper interface.

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ObjectFactory;
import org.mapstruct.factory.Mappers;

@Mapper
public interface VehicleMapperWithInlineFactory {

    VehicleMapperWithInlineFactory INSTANCE =
            Mappers.getMapper(VehicleMapperWithInlineFactory.class);

    @Mapping(target = "model", source = "model")
    Vehicle toVehicle(VehicleDTO dto);

    @ObjectFactory
    default Vehicle createVehicle(VehicleDTO dto) {

        if ("car".equalsIgnoreCase(dto.getType())) {
            return new Car();
        } 
        else if ("bike".equalsIgnoreCase(dto.getType())) {
            return new Bike();
        }

        throw new IllegalArgumentException("Unknown vehicle type");
    }
}

The @ObjectFactory method ensures that MapStruct instantiates the correct concrete class internally without requiring a separate factory class.

3.7 Test Class

This test program executes both the external factory and inline factory mappings to verify the output.

public class TestApplication {

    public static void main(String[] args) {

        VehicleDTO dto = new VehicleDTO();
        dto.setType("car");
        dto.setModel("Tesla Model 3");

        // External Factory Mapping
        Vehicle vehicle1 =
            VehicleMapperWithExternalFactory.INSTANCE.toVehicle(dto);

        System.out.println("---- External Factory Output ----");
        System.out.println("Vehicle Type  : " + vehicle1.getVehicleType());
        System.out.println("Vehicle Model : " + vehicle1.getModel());

        // Inline Factory Mapping
        Vehicle vehicle2 =
            VehicleMapperWithInlineFactory.INSTANCE.toVehicle(dto);

        System.out.println("---- Inline Factory Output ----");
        System.out.println("Vehicle Type  : " + vehicle2.getVehicleType());
        System.out.println("Vehicle Model : " + vehicle2.getModel());
    }
}

This execution confirms that both approaches correctly create a Car object and map the model value without instantiation issues caused by the abstract target class.

3.8 Code Run and Output

---- External Factory Output ----
Vehicle Type  : Car
Vehicle Model : Tesla Model 3

---- Inline Factory Output ----
Vehicle Type  : Car
Vehicle Model : Tesla Model 3

The output confirms that both the external factory and inline @ObjectFactory approaches correctly instantiate the Car subclass and map the model field as expected without any runtime errors.

4. Conclusion

Mapping abstract classes in MapStruct is not supported directly because abstract types cannot be instantiated. However, this limitation is solved cleanly by using:

  • External factory classes with @Mapper(uses = Factory.class)
  • Inline factory methods using @ObjectFactory

These approaches allow you to map polymorphic hierarchies efficiently, making MapStruct an excellent choice for enterprise-grade DTO-to-entity transformations.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

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

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button