Java annotation processor for the generation of type-safe bean mappers
—
Hooks and customization mechanisms for extending mapper behavior with custom logic, object factories, and lifecycle callbacks.
Lifecycle hooks that allow custom logic execution before and after mapping operations.
/**
* Marks a method to be invoked at the end of a generated mapping method.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@interface BeforeMapping {
}
/**
* Marks a method to be invoked at the end of a generated mapping method.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@interface AfterMapping {
}Usage Examples:
@Mapper
public abstract class UserMapper {
// Abstract mapping method
public abstract UserDto toUserDto(User user);
// Before mapping hook
@BeforeMapping
protected void validateUser(User user) {
if (user.getEmail() == null) {
throw new IllegalArgumentException("User email cannot be null");
}
}
// After mapping hook
@AfterMapping
protected void enrichUserDto(User user, @MappingTarget UserDto userDto) {
// Add computed fields
userDto.setDisplayName(user.getFirstName() + " " + user.getLastName());
userDto.setAccountAge(calculateAccountAge(user.getCreatedDate()));
// Apply business logic
if (user.isPremiumMember()) {
userDto.setBadge("PREMIUM");
}
}
private int calculateAccountAge(LocalDate createdDate) {
return Period.between(createdDate, LocalDate.now()).getYears();
}
}Custom object creation for target instances instead of using default constructors.
/**
* Marks a method as factory method for creating instances of the return type.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@interface ObjectFactory {
}Usage Examples:
@Mapper
public abstract class OrderMapper {
// Abstract mapping method
public abstract OrderDto toOrderDto(Order order);
// Object factory for creating OrderDto instances
@ObjectFactory
protected OrderDto createOrderDto(Order order) {
// Custom creation logic based on source
if (order.getType() == OrderType.EXPRESS) {
return new ExpressOrderDto();
} else if (order.getType() == OrderType.BULK) {
return new BulkOrderDto();
} else {
return new StandardOrderDto();
}
}
// Factory with qualifier
@ObjectFactory
@Named("createAuditableDto")
protected AuditableDto createAuditableDto() {
AuditableDto dto = new AuditableDto();
dto.setCreatedAt(Instant.now());
dto.setCreatedBy(getCurrentUser());
return dto;
}
// Using qualified factory
@Mapping(target = ".", qualifiedByName = "createAuditableDto")
AuditableDto toAuditableDto(Entity entity);
private String getCurrentUser() {
// Implementation to get current user
return SecurityContext.getCurrentUser();
}
}
// Example with dependency injection
@Mapper(componentModel = "spring")
public abstract class SpringOrderMapper {
@Autowired
private OrderFactory orderFactory;
public abstract OrderDto toOrderDto(Order order);
@ObjectFactory
protected OrderDto createOrderDto(Order order) {
return orderFactory.createDto(order.getType());
}
}Allows decoration of generated mappers with additional custom logic.
/**
* Marks a mapper to be decorated with the class specified via DecoratedWith.value().
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@interface DecoratedWith {
/** Decorator class */
Class<?> value();
}Usage Examples:
// Base mapper interface
@Mapper
@DecoratedWith(PersonMapperDecorator.class)
public interface PersonMapper {
PersonDto toPersonDto(Person person);
List<PersonDto> toPersonDtos(List<Person> persons);
}
// Decorator class
public abstract class PersonMapperDecorator implements PersonMapper {
private final PersonMapper delegate;
public PersonMapperDecorator(PersonMapper delegate) {
this.delegate = delegate;
}
@Override
public PersonDto toPersonDto(Person person) {
// Call the generated mapper
PersonDto dto = delegate.toPersonDto(person);
// Add custom logic
if (person.getAge() >= 18) {
dto.setLegalStatus("ADULT");
} else {
dto.setLegalStatus("MINOR");
}
// Add external service integration
dto.setCreditScore(creditScoreService.getScore(person.getSsn()));
return dto;
}
@Override
public List<PersonDto> toPersonDtos(List<Person> persons) {
// Custom batch processing logic
List<PersonDto> dtos = delegate.toPersonDtos(persons);
// Batch enrich with external data
enrichWithExternalData(dtos);
return dtos;
}
private void enrichWithExternalData(List<PersonDto> dtos) {
// Implementation for batch enrichment
}
}
// Spring integration example
@Component
public abstract class SpringPersonMapperDecorator implements PersonMapper {
@Autowired
private PersonMapper delegate;
@Autowired
private ExternalService externalService;
@Override
public PersonDto toPersonDto(Person person) {
PersonDto dto = delegate.toPersonDto(person);
dto.setExternalData(externalService.getData(person.getId()));
return dto;
}
}Annotations for marking method parameters with special roles in mapping operations.
/**
* Marks a parameter of a mapping method as target for the mapping.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.CLASS)
@interface MappingTarget {
}
/**
* Marks a parameter as mapping context.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.CLASS)
@interface Context {
}
/**
* Provides the target type to select a mapping method.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.CLASS)
@interface TargetType {
}
/**
* Provides the source property name available to the annotated parameter.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.CLASS)
@interface SourcePropertyName {
}
/**
* Provides the target property name available to the annotated parameter.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.CLASS)
@interface TargetPropertyName {
}Usage Examples:
@Mapper
public interface ParameterMapper {
// Update mapping using @MappingTarget
void updatePersonDto(Person person, @MappingTarget PersonDto dto);
// Mapping with context
PersonDto toPersonDto(Person person, @Context MappingContext context);
// Method using context in custom logic
default String formatName(String firstName, String lastName, @Context MappingContext context) {
if (context.getLocale().getLanguage().equals("ja")) {
return lastName + " " + firstName;
} else {
return firstName + " " + lastName;
}
}
// Generic mapping with target type selection
<T> T mapToType(Object source, @TargetType Class<T> targetType);
// Custom property mapping methods
default String customPropertyMapper(
String value,
@SourcePropertyName String sourceProperty,
@TargetPropertyName String targetProperty
) {
return String.format("[%s->%s]: %s", sourceProperty, targetProperty, value);
}
}
// Context class example
public class MappingContext {
private Locale locale;
private String currentUser;
private Map<String, Object> attributes;
// constructors, getters, setters...
public Locale getLocale() { return locale; }
public String getCurrentUser() { return currentUser; }
public Map<String, Object> getAttributes() { return attributes; }
}
// Usage with context
MappingContext context = new MappingContext();
context.setLocale(Locale.JAPANESE);
context.setCurrentUser("admin");
PersonDto dto = mapper.toPersonDto(person, context);Conditional mapping capabilities for controlling when mappings should be applied.
/**
* Marks a method as condition check for source property.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@interface Condition {
}
/**
* Marks a parameter to be used for source condition checking.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.CLASS)
@interface SourceParameterCondition {
}Usage Examples:
@Mapper
public abstract class ConditionalMapper {
// Main mapping method
@Mapping(target = "email", conditionQualifiedByName = "isValidEmail")
@Mapping(target = "phone", conditionQualifiedByName = "hasValidPhone")
public abstract ContactDto toContactDto(Person person);
// Condition methods
@Condition
@Named("isValidEmail")
protected boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.contains(".");
}
@Condition
@Named("hasValidPhone")
protected boolean hasValidPhone(Person person) {
return person.getPhone() != null && person.getPhone().matches("\\d{10}");
}
// Source parameter condition
@Condition
protected boolean shouldMapField(@SourceParameterCondition Person person, String value) {
return person.isActive() && value != null;
}
// Using expression-based conditions
@Mapping(
target = "displayName",
source = "name",
conditionExpression = "java(person.getName().length() > 2)"
)
@Mapping(
target = "age",
source = "birthDate",
conditionExpression = "java(person.getBirthDate() != null)"
)
public abstract ProfileDto toProfileDto(Person person);
}Defining custom mapping methods for specific transformations.
Usage Examples:
@Mapper
public abstract class CustomMethodMapper {
// Abstract mapping method
public abstract ProductDto toProductDto(Product product);
// Custom mapping for specific field types
protected String formatPrice(BigDecimal price) {
if (price == null) return "N/A";
return NumberFormat.getCurrencyInstance().format(price);
}
protected LocalDate stringToDate(String dateString) {
if (dateString == null || dateString.trim().isEmpty()) {
return null;
}
return LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE);
}
protected String dateToString(LocalDate date) {
return date != null ? date.format(DateTimeFormatter.ISO_LOCAL_DATE) : null;
}
// Qualified custom methods
@Named("toUpperCase")
protected String toUpperCase(String value) {
return value != null ? value.toUpperCase() : null;
}
// Using custom methods in mappings
@Mapping(target = "formattedPrice", source = "price")
@Mapping(target = "name", source = "title", qualifiedByName = "toUpperCase")
public abstract ProductSummaryDto toProductSummary(Product product);
}Install with Tessl CLI
npx tessl i tessl/maven-org-mapstruct--mapstruct