MapStruct's @DecoratedWith annotation enables extending generated mappers with custom business logic while preserving core mapping functionality.

Domain Models

Person.java

public class Person {
    private String firstName;
    private String lastName;
    private Address address;

    public Person() {}

    public Person(String firstName, String lastName, Address address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.address = address;
    }

    // Getters and setters
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
    public Address getAddress() { return address; }
    public void setAddress(Address address) { this.address = address; }
}

Address.java

public class Address {
    private String street;
    private String city;

    public Address() {}
    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }

    public String getStreet() { return street; }
    public void setStreet(String street) { this.street = street; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
}

DTO Classes

PersonDto.java

public class PersonDto {
    private String name;  // Computed by decorator
    private AddressDto address;

    public PersonDto() {}

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public AddressDto getAddress() { return address; }
    public void setAddress(AddressDto address) { this.address = address; }
}

AddressDto.java

public class AddressDto {
    private String street;
    private String city;

    public AddressDto() {}

    public String getStreet() { return street; }
    public void setStreet(String street) { this.street = street; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
}

Mapper Interface

PersonMapper.java

import org.mapstruct.*;

@Mapper
@DecoratedWith(PersonMapperDecorator.class)
public interface PersonMapper {
    @Mapping(target = "name", ignore = true)  // Handled by decorator
    PersonDto personToPersonDto(Person person);

    AddressDto addressToAddressDto(Address address);
}

Decorator Implementation

PersonMapperDecorator.java

public abstract class PersonMapperDecorator implements PersonMapper {
    protected final PersonMapper delegate;

    public PersonMapperDecorator(PersonMapper delegate) {
        this.delegate = delegate;
    }

    @Override
    public PersonDto personToPersonDto(Person person) {
        // Delegate basic mapping first
        PersonDto dto = delegate.personToPersonDto(person);

        // Custom business logic: concatenate names
        dto.setName(person.getFirstName() + " " + person.getLastName());

        return dto;
    }

    // Other methods delegate automatically (remains abstract)
}

Usage Example

Main.java

import org.mapstruct.factory.Mappers;

public class Main {
    public static void main(String[] args) {
        PersonMapper mapper = Mappers.getMapper(PersonMapper.class);

        Address address = new Address("123 Main St", "Springfield");
        Person person = new Person("John", "Doe", address);

        // Uses decorator: name becomes "John Doe"
        PersonDto dto = mapper.personToPersonDto(person);
        System.out.println("Name: " + dto.getName());           // John Doe
        System.out.println("Street: " + dto.getAddress().getStreet());  // 123 Main St

        // Direct delegation (no custom logic)
        AddressDto addrDto = mapper.addressToAddressDto(address);
        System.out.println("City: " + addrDto.getCity());       // Springfield
    }
}

How It Works

MapStruct generates PersonMapperImpl handling all standard mappings. The decorator wraps this implementation via constructor injection. Mappers.getMapper() automatically returns the decorator instance containing the generated delegate. Only overridden methods (personToPersonDto) execute custom logic; others delegate transparently. This pattern ensures thread-safety, testability, and framework independence.