MapStruct's @Context annotation enables passing contextual data—like locales, services, or user state—through mapper method calls without treating it as source or target input. This propagates automatically to lifecycle methods (@BeforeMapping, @AfterMapping, @ObjectFactory) and custom qualifiers, making it ideal for complex mappings requiring external dependencies.

Core Concepts

@Context marks parameters for transparent propagation: upstream mapper methods forward them to downstream calls, including generated code, custom methods, and lifecycle hooks. Callers must supply values explicitly, as MapStruct performs no instantiation or defaulting. Propagation requires matching @Context declarations across methods, with type compatibility enforced.

Key benefits include locale-aware formatting, injecting services for lookups, or stateful transformations—all without cluttering method signatures with non-mapping data.

Complete Runnable Example

Save as ContextDemo.java.

import org.mapstruct.*;
import org.mapstruct.factory.Mappers;

import java.util.Locale;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;

// Source and Target DTOs
class Car {
    private String manufacturer;
    private String model;
    private LocalDate built;

    public Car(String manufacturer, String model, LocalDate built) {
        this.manufacturer = manufacturer;
        this.model = model;
        this.built = built;
    }

    // Getters/Setters
    public String getManufacturer() { return manufacturer; }
    public void setManufacturer(String manufacturer) { this.manufacturer = manufacturer; }
    public String getModel() { return model; }
    public void setModel(String model) { this.model = model; }
    public LocalDate getBuilt() { return built; }
    public void setBuilt(LocalDate built) { this.built = built; }
}

class CarDto {
    private String make;
    private String fullModel;
    private String formattedBuildDate;
    private String contextInfo;

    // Getters/Setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }
    public String getFullModel() { return fullModel; }
    public void setFullModel(String fullModel) { this.fullModel = fullModel; }
    public String getFormattedBuildDate() { return formattedBuildDate; }
    public void setFormattedBuildDate(String formattedBuildDate) { this.formattedBuildDate = formattedBuildDate; }
    public String getContextInfo() { return contextInfo; }
    public void setContextInfo(String contextInfo) { this.contextInfo = contextInfo; }
}

// Mapper with full lifecycle + ObjectFactory
@Mapper(uses = LocalDateUtils.class)
abstract class CarMapper {
    // Top-level entry point - caller provides @Context
    @Mapping(source = "manufacturer", target = "make")
    @Mapping(source = "built", target = "formattedBuildDate", qualifiedByName = "formatDate")
    @Mapping(target = "fullModel", ignore = true)  // Set in @AfterMapping
    @Mapping(target = "contextInfo", ignore = true)  // Set in @ObjectFactory
    public abstract CarDto toDto(Car car, @Context Locale locale, @Context String userId);

    // ObjectFactory receives @Context for custom instantiation
    @ObjectFactory
    public CarDto createCarDto(@Context Locale locale, @Context String userId) {
        CarDto dto = new CarDto();
        dto.setContextInfo("Created for user: " + userId + " in locale: " + locale.getDisplayName());
        System.out.println("ObjectFactory: Initialized DTO with context - " + userId);
        return dto;
    }

    // Lifecycle methods receive @Context automatically
    @BeforeMapping
    void beforeMapping(Car car, @MappingTarget CarDto dto, @Context Locale locale, @Context String userId) {
        System.out.println("BeforeMapping: Pre-processing with " + locale.getDisplayName() + " for user " + userId);
    }

    @AfterMapping
    void afterMapping(Car car, @MappingTarget CarDto dto, @Context Locale locale, @Context String userId) {
        System.out.println("AfterMapping: Finalizing with " + locale.getDisplayName() + " for user " + userId);
        dto.setFullModel(dto.getMake() + " " + car.getModel() + " (" + locale.getCountry() + ")");
    }
}

// Custom qualifier helper (also receives @Context)
class LocalDateUtils {
    @Named("formatDate")
    public static String format(LocalDate date, @Context Locale locale) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM yyyy", locale);
        return date.format(formatter);
    }
}

// Demo runner
class ContextDemo {
    public static void main(String[] args) {
        CarMapper mapper = Mappers.getMapper(CarMapper.class);

        Car car = new Car("Toyota", "Camry", LocalDate.of(2020, 5, 15));

        // Provide @Context explicitly (Locale + custom userId)
        CarDto dto = mapper.toDto(car, Locale.forLanguageTag("de-DE"), "john.doe@company.com");

        System.out.println("\n=== Final Result ===");
        System.out.println("Make: " + dto.getMake());
        System.out.println("Full Model: " + dto.getFullModel());
        System.out.println("Build Date: " + dto.getFormattedBuildDate());
        System.out.println("Context Info: " + dto.getContextInfo());
    }
}

Expected output showing full lifecycle flow:

ObjectFactory: Initialized DTO with context - john.doe@company.com
BeforeMapping: Pre-processing with German (Germany) for user john.doe@company.com
AfterMapping: Finalizing with German (Germany) for user john.doe@company.com

=== Final Result ===
Make: Toyota
Full Model: Toyota Camry (DE)
Build Date: Mai 2020
Context Info: Created for user: john.doe@company.com in locale: German (Germany)

Lifecycle Integration Details

The complete flow works as: ObjectFactory@BeforeMappingproperty mappings@AfterMapping. Each receives @Context parameters automatically when declared with matching signatures. @ObjectFactory handles target instantiation (useful for dependency injection), while lifecycle methods enable pre/post-processing with full context awareness.

This pattern scales perfectly for enterprise applications requiring audit trails, localization, or service lookups during mapping.