Mapper composition in MapStruct creates modular mappings by delegating between mappers using @Mapper(uses = {...}). The examples below are fully self-contained, compilable classes with complete domain models, mappers, and main methods.
Complete Domain Classes
import java.time.LocalDate;
// Source domain classes
public class Address {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
// Getters and setters
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; }
}
public class User {
private String name;
private Address address;
public User(String name, Address address) {
this.name = name;
this.address = address;
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Address getAddress() { return address; }
public void setAddress(Address address) { this.address = address; }
}
// Extended User for annotation example
public class UserEnriched {
private String firstName;
private String lastName;
private int age;
private Address address;
public UserEnriched(String firstName, String lastName, int age, Address address) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.address = address;
}
// Getters
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public Address getAddress() { return address; }
}
// Target DTO classes
public class AddressDto {
private String street;
private String city;
public AddressDto() {}
// Getters and setters
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; }
@Override
public String toString() {
return street + ", " + city;
}
}
public class UserDto {
private String name;
private AddressDto address;
public UserDto() {}
// Getters and setters
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; }
@Override
public String toString() {
return name + " <" + address + ">";
}
}
public class UserEnrichedDto {
private String fullName;
private String ageGroup;
private AddressDto address;
public UserEnrichedDto() {}
// Getters and setters
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public String getAgeGroup() { return ageGroup; }
public void setAgeGroup(String ageGroup) { this.ageGroup = ageGroup; }
public AddressDto getAddress() { return address; }
public void setAddress(AddressDto address) { this.address = address; }
@Override
public String toString() {
return fullName + " (" + ageGroup + ") <" + address + ">";
}
}
Basic Composition Example
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface AddressMapper {
AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
AddressDto toDto(Address address);
}
@Mapper(uses = AddressMapper.class)
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserDto toDto(User user);
}
// Runnable test
public class BasicComposition {
public static void main(String[] args) {
User user = new User("John Doe",
new Address("123 Main St", "Auckland"));
UserDto dto = UserMapper.INSTANCE.toDto(user);
System.out.println(dto);
// Output: John Doe <123 Main St, Auckland>
}
}
Multiple Mapper and Custom Conversion Composition
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;
public class DateUtils {
public static String toString(LocalDate date) {
if (date == null) {
return null;
}
return date.format(DateTimeFormatter.ISO_LOCAL_DATE);
}
}
public class Event {
private String title;
private LocalDate date;
private Address address;
public Event(String title, LocalDate date, Address address) {
this.title = title;
this.date = date;
this.address = address;
}
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public LocalDate getDate() { return date; }
public void setDate(LocalDate date) { this.date = date; }
public Address getAddress() { return address; }
public void setAddress(Address address) { this.address = address; }
}
public class EventDto {
private String title;
private String dateString;
private AddressDto address;
public EventDto() {}
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDateString() { return dateString; }
public void setDateString(String dateString) { this.dateString = dateString; }
public AddressDto getAddress() { return address; }
public void setAddress(AddressDto address) { this.address = address; }
@Override
public String toString() {
return title + " on " + dateString + " at " + address;
}
}
@Mapper(uses = {AddressMapper.class, DateUtils.class})
public interface EventMapper {
EventMapper INSTANCE = Mappers.getMapper(EventMapper.class);
@Mapping(source = "date", target = "dateString")
EventDto toDto(Event event);
}
// Runnable test
public class CustomComposition {
public static void main(String[] args) {
Event event = new Event("Tech Conference",
LocalDate.of(2026, 3, 15),
new Address("Convention Centre", "Auckland"));
EventDto dto = EventMapper.INSTANCE.toDto(event);
System.out.println(dto);
// Output: Tech Conference on 2026-03-15 at Convention Centre, Auckland
}
}
Advanced: Reusable Annotation Composition
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.Qualifier;
import org.mapstruct.factory.Mappers;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Custom utility for age grouping
public class AgeUtils {
@Named("ageToGroup")
public static String ageToGroup(int age) {
if (age < 18) return "Minor";
if (age < 65) return "Adult";
return "Senior";
}
}
// 1. Custom composed annotation (bundles multiple @Mapping rules)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
@Mapping(target = "ageGroup", source = "age", qualifiedByName = "ageToGroup")
public @interface UserEnrichment {}
// 2. Mapper using the composed annotation
@Mapper(uses = {AddressMapper.class, AgeUtils.class})
public interface UserEnrichedMapper {
UserEnrichedMapper INSTANCE = Mappers.getMapper(UserEnrichedMapper.class);
@UserEnrichment // <- Single annotation replaces 3+ @Mapping entries!
UserEnrichedDto toEnrichedDto(UserEnriched user);
}
// Runnable test
public class AnnotationComposition {
public static void main(String[] args) {
UserEnriched user = new UserEnriched(
"John", "Doe", 42,
new Address("123 Main St", "Auckland")
);
UserEnrichedDto dto = UserEnrichedMapper.INSTANCE.toEnrichedDto(user);
System.out.println(dto);
// Output: John Doe (Adult) <123 Main St, Auckland>
}
}
Global Configuration
See the Shared MapperConfig in MapStruct: Pure Java
Key Benefits Summary
| Feature | Pure Java Benefit |
|---|---|
uses = {...} |
Automatic instantiation, no DI container |
Mappers.getMapper() |
Single point of access per mapper |
@MapperConfig |
Consistent behavior without framework |
Multiple uses |
Type-based delegation selection |
@UserEnrichment |
Zero-boilerplate reusable mapping rules |
| Custom utils | Seamless integration with plain classes |
Each example compiles and runs independently after MapStruct annotation processing, demonstrating self-contained mapper hierarchies without external dependencies.
Leave a Reply