MapStruct's @MappingTarget and @TargetType annotations solve distinct mapping challenges. @MappingTarget enables in-place updates of existing objects, while @TargetType provides runtime type information for dynamic object creation.

Core Concepts

@MappingTarget marks a parameter as an existing target for modification rather than creating a new instance. Use it for DTO-to-entity updates where the target object already exists in memory (e.g., from a database).

@TargetType injects the concrete target Class<T> as a parameter, essential for generic factories and lifecycle methods where compile-time type information is erased.

Complete Working Example

import org.mapstruct.*;
import java.lang.reflect.Constructor;

// 1. Plain Java DTOs and Entities
class CarDto {
    private String make;
    private String model;
    private int year;

    public CarDto() {}

    public CarDto(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    // Getters and setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }

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

    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }
}

class CarEntity {
    private String make;
    private String model;
    private int year;
    private String vin;  // Entity-specific field

    public CarEntity() {}

    // Getters and setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }

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

    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }

    public String getVin() { return vin; }
    public void setVin(String vin) { this.vin = vin; }
}

// 2. @TargetType Custom Factory
class CarEntityFactory {
    public <T extends CarEntity> T createEntity(@TargetType Class<T> entityClass) {
        try {
            Constructor<T> constructor = entityClass.getDeclaredConstructor();
            T entity = constructor.newInstance();
            entity.setVin("VIN-" + entityClass.getSimpleName());
            return entity;
        } catch (Exception e) {
            throw new RuntimeException("Cannot create entity: " + entityClass.getName(), e);
        }
    }
}

// 3. Mapper Demonstrating Both Annotations
@Mapper(uses = CarEntityFactory.class)
interface CarMapper {

    // Creates NEW object using @TargetType factory
    CarEntity toEntity(CarDto dto);

    // Updates EXISTING object - @MappingTarget required
    void updateEntity(@MappingTarget CarEntity entity, CarDto dto);

    // Updates and returns - also uses @MappingTarget
    CarEntity updateAndReturn(@MappingTarget CarEntity entity, CarDto dto);
}

// 4. Advanced Mapper with Lifecycle Methods
@Mapper(uses = CarEntityFactory.class)
abstract class AdvancedCarMapper {

    public abstract CarEntity toEntity(CarDto dto);

    @BeforeMapping
    void beforeUpdate(CarDto source,
                              @MappingTarget CarEntity target,
                              @TargetType Class<?> targetType) {
        System.out.println("🔄 Before: Updating " + targetType.getSimpleName());
        System.out.println("   Target VIN before: " + target.getVin());
    }

    @AfterMapping
    void afterUpdate(CarDto source,
                             @MappingTarget CarEntity target,
                             @TargetType Class<?> targetType) {
        System.out.println("✅ After: Updated " + targetType.getSimpleName());
        System.out.println("   Target VIN after: " + target.getVin());
    }
}

Usage Demonstration

import org.mapstruct.factory.Mappers;

public class MapStructDemo {
    public static void main(String[] args) {
        CarMapper mapper = Mappers.getMapper(CarMapper.class);
        AdvancedCarMapper advancedMapper = Mappers.getMapper(AdvancedCarMapper.class);

        System.out.println("=== 1. NEW Object Creation (uses @TargetType factory) ===");
        CarDto newDto = new CarDto("Toyota", "Camry", 2023);
        CarEntity newEntity = mapper.toEntity(newDto);
        System.out.println("New entity: " + newEntity.getMake() + " VIN: " + newEntity.getVin());
        // Output: VIN-CarEntity

        System.out.println("\n=== 2. Update Existing (uses @MappingTarget) ===");
        CarEntity existingEntity = new CarEntity();
        existingEntity.setVin("EXISTING-123");
        CarDto updateDto = new CarDto("Honda", "Civic", 2022);

        mapper.updateEntity(existingEntity, updateDto);
        System.out.println("Updated: " + existingEntity.getModel() + " VIN preserved: " + existingEntity.getVin());
        // VIN preserved! Only mapped fields updated

        System.out.println("\n=== 3. Lifecycle Hooks (both annotations) ===");
        advancedMapper.toEntity(new CarDto("Ford", "Mustang", 2024));
        // Prints before/after messages with type info
    }
}

Expected Output

=== 1. NEW Object Creation (uses @TargetType factory) ===
New entity: Toyota VIN: VIN-CarEntity

=== 2. Update Existing (uses @MappingTarget) ===
Updated: Civic VIN preserved: EXISTING-123

=== 3. Lifecycle Hooks (both annotations) ===
🔄 Before: Updating CarEntity
   Target VIN before: null
✅ After: Updated CarEntity
   Target VIN after: VIN-CarEntity

Decision Matrix: When to Use Each

Scenario @MappingTarget @TargetType
DTO → New Entity ❌ No ✅ Factory methods
Partial DTO Update ✅ Always ❌ No
Generic Collections ❌ No ✅ Essential
Lifecycle Processing ✅ Target param ✅ Type param
Polymorphic Mapping ❌ No ✅ Required

This pure Java example demonstrates both annotations in realistic enterprise scenarios, showing the clear distinction between new object creation and in-place updates.