Learn Java Data Mapper Json Xml Validation Part 023 Mapstruct Basic To Advanced Mapping
title: Learn Java Data Mapper, JSON/XML Processing & Validation - Part 023 description: MapStruct mapping deep dive untuk field, nested object, collections, maps, enums, constants, default values, expressions, qualifiers, multiple source parameters, and production-safe conversion. series: learn-java-data-mapper-json-xml-validation seriesTitle: Learn Java Data Mapper, JSON/XML Processing & Validation order: 23 partTitle: MapStruct Mapping: Fields, Nested Objects, Collections, Enums, Expressions, Constants tags:
- java
- mapstruct
- data-mapper
- dto
- nested-mapping
- collection-mapping
- enum-mapping
- expressions
- constants
- qualifiers date: 2026-06-29
Part 023 — MapStruct Mapping: Fields, Nested Objects, Collections, Enums, Expressions, Constants
Target skill: mampu menggunakan MapStruct untuk mapping field, nested object, collection, map, enum, constants, default values, expressions, qualifiers, dan multiple source parameters tanpa kehilangan semantic correctness.
Part 022 membahas arsitektur MapStruct: annotation processing, generated code, compile-time safety. Sekarang kita masuk ke feature-level mapping yang paling sering dipakai di proyek nyata.
Mental model utama:
MapStruct maps structure. You define meaning.
Jika nama field sama dan meaning sama, convention mapping cukup. Jika nama sama tapi meaning berbeda, buat mapping eksplisit. Jika nama berbeda tapi meaning sama, @Mapping menjembatani. Jika transformasi membawa policy, pindahkan ke value object/domain service atau mapper support method yang jelas.
1. Kaufman Deconstruction
Subskill Part 023:
| Subskill | Kemampuan |
|---|---|
| Basic property mapping | Mengandalkan same-name mapping secara aman |
| Explicit field mapping | Menggunakan @Mapping(target, source) |
| Nested source mapping | Mapping source = "customer.id.value" |
| Nested target mapping | Mapping ke object target nested |
| Flattening/composition | Menggabungkan nested source ke flat DTO atau sebaliknya |
| Collection mapping | Mapping List, Set, array, iterable |
| Map mapping | Mapping Map<K,V> dengan key/value conversion |
| Enum mapping | Menggunakan @ValueMapping, MappingConstants, explicit code mapping |
| Constants/defaults | Memakai constant, defaultValue, defaultExpression dengan hati-hati |
| Expressions | Memakai expression = "java(...)" hanya untuk transformation kecil |
| Qualifiers | Memilih conversion method dengan @Named / qualifier |
| Multiple source params | Mapping dari beberapa input objects |
| Test mapping intent | Unit/golden/enum coverage/null policy tests |
2. Basic Same-Name Mapping
Source:
public record Customer(
String customerId,
String fullName,
String email
) {}
Target:
public record CustomerResponse(
String customerId,
String fullName,
String email
) {}
Mapper:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CustomerMapper {
CustomerResponse toResponse(Customer customer);
}
MapStruct maps same-name compatible properties automatically.
Generated logic conceptually:
return new CustomerResponse(
customer.customerId(),
customer.fullName(),
customer.email()
);
Use convention when:
- names match
- types match or conversion is obvious and safe
- meaning matches
- target is not security-sensitive
- tests cover output shape
Do not rely on convention if semantic differs.
3. Explicit Property Mapping
Source:
public record Customer(
CustomerId id,
String legalName
) {}
Target:
public record CustomerResponse(
String customerId,
String fullName
) {}
Mapper:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CustomerMapper {
@Mapping(target = "customerId", source = "id.value")
@Mapping(target = "fullName", source = "legalName")
CustomerResponse toResponse(Customer customer);
}
This documents meaning.
Review question for every explicit mapping:
Is this name translation, type conversion, semantic conversion, or business decision?
- name translation: good
- type conversion: good if deterministic
- semantic conversion: okay if local and tested
- business decision: usually not mapper responsibility
4. Nested Source Mapping
Source:
public record Order(
OrderId id,
Customer customer,
Money total
) {}
public record Customer(
CustomerId id,
String fullName
) {}
Target:
public record OrderResponse(
String orderId,
String customerId,
String customerName,
String totalAmount,
String currency
) {}
Mapper:
@Mapper(
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = MoneyMapper.class
)
public interface OrderMapper {
@Mapping(target = "orderId", source = "id.value")
@Mapping(target = "customerId", source = "customer.id.value")
@Mapping(target = "customerName", source = "customer.fullName")
@Mapping(target = "totalAmount", source = "total.amount")
@Mapping(target = "currency", source = "total.currency.currencyCode")
OrderResponse toResponse(Order order);
}
MapStruct generates null checks for nested paths depending configuration and source shape.
Be careful:
- nested source can hide object graph traversal
- lazy loaded entity relations can be triggered
- deep nested mapping can leak internal shape
- flattening should be intentional projection
5. Nested Target Mapping
Target:
public class OrderDto {
private String orderId;
private CustomerDto customer;
private MoneyDto total;
// getters/setters
}
Mapper:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface OrderMapper {
@Mapping(target = "orderId", source = "id.value")
@Mapping(target = "customer.customerId", source = "customer.id.value")
@Mapping(target = "customer.fullName", source = "customer.fullName")
@Mapping(target = "total.amount", source = "total.amount")
@Mapping(target = "total.currency", source = "total.currency.currencyCode")
OrderDto toDto(Order order);
}
This creates nested target objects if MapStruct can instantiate them.
Review generated code to ensure:
- target nested objects are created
- null source handling matches expectation
- no unexpected target overwrite
- constructor/setter/builder path is correct
6. Flattening and Composition
Flattening:
public record CustomerProfile(
Customer customer,
Address address,
RiskProfile risk
) {}
Target:
public record CustomerProfileResponse(
String customerId,
String fullName,
String city,
String riskSegment
) {}
Mapper:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CustomerProfileMapper {
@Mapping(target = "customerId", source = "customer.id.value")
@Mapping(target = "fullName", source = "customer.fullName")
@Mapping(target = "city", source = "address.city")
@Mapping(target = "riskSegment", source = "risk.segment")
CustomerProfileResponse toResponse(CustomerProfile profile);
}
Composition from multiple source parameters:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CustomerProfileMapper {
@Mapping(target = "customerId", source = "customer.id.value")
@Mapping(target = "fullName", source = "customer.fullName")
@Mapping(target = "city", source = "address.city")
@Mapping(target = "riskSegment", source = "risk.segment")
CustomerProfileResponse toResponse(
Customer customer,
Address address,
RiskProfile risk
);
}
Use multiple source parameters when the projection is assembled from multiple read models.
7. Mapping target = "."
MapStruct supports mapping nested bean properties to current target using target = ".".
Example:
public record CustomerRecord(
Customer customer,
Address address
) {}
public record CustomerFlatDto(
String name,
String street,
String city
) {}
Mapper:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CustomerFlatMapper {
@Mapping(target = ".", source = "customer")
@Mapping(target = ".", source = "address")
CustomerFlatDto toDto(CustomerRecord record);
}
This can reduce repetitive mapping for flattening, but it can also hide conflicts.
Use when:
- source beans have clearly disjoint property names
- conflict resolution is explicit
- generated code is inspected
- tests cover field mapping
Avoid if it makes mapping less readable.
8. Constants
Set fixed target value:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface EventMapper {
@Mapping(target = "schemaVersion", constant = "1")
@Mapping(target = "eventType", constant = "customer.created")
CustomerCreatedEvent toEvent(Customer customer);
}
Good use:
- event type
- schema version
- source system code
- fixed discriminator
- technical contract field
Bad use:
@Mapping(target = "status", constant = "APPROVED")
if approval is a domain decision.
9. Default Values
defaultValue applies when source is null.
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CustomerMapper {
@Mapping(target = "displayName", source = "fullName", defaultValue = "Unknown")
CustomerResponse toResponse(Customer customer);
}
Use with caution.
Question:
Is default part of presentation, contract compatibility, or business truth?
Default value is acceptable for:
- display fallback
- legacy contract requiring field
- optional label
- system code when source absent by design
Avoid for:
- money amount
- identity
- workflow status
- timestamp
- regulatory facts
- permissions
Silent defaults can corrupt meaning.
10. Default Expression
@Mapper(imports = UUID.class)
public interface RequestMapper {
@Mapping(
target = "requestId",
source = "requestId",
defaultExpression = "java(UUID.randomUUID().toString())"
)
InternalRequest toInternal(ApiRequest request);
}
Use sparingly.
For request IDs, timestamps, current user, tenant, and audit fields, prefer use-case layer or explicit context object so behavior is testable and deterministic.
Better:
CreatePaymentCommand toCommand(
CreatePaymentRequest request,
@Context RequestContext context
);
or pass generated value as source:
@Mapping(target = "requestId", source = "requestId")
Command toCommand(Request request, RequestId requestId);
11. Expressions
Expression example:
@Mapper(imports = {Currency.class})
public interface MoneyMapper {
@Mapping(
target = "currency",
expression = "java(Currency.getInstance(dto.currency()))"
)
Money toMoney(MoneyDto dto);
}
Expressions are powerful but less refactor-safe than normal methods.
Prefer named default methods:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface MoneyMapper {
@Mapping(target = "currency", source = "currency")
Money toMoney(MoneyDto dto);
default Currency toCurrency(String code) {
return code == null ? null : Currency.getInstance(code);
}
}
This is easier to test and reuse.
Use expressions for:
- tiny glue code
- constructor call not expressible otherwise
- constant object creation
- simple derived field
Avoid expressions for:
- complex branching
- service calls
- database lookup
- permission logic
- workflow transition
- feature flags
12. Qualifiers with @Named
Problem: two ways to map same type.
public class DateTimeMapper {
@Named("isoInstant")
public Instant isoInstant(String value) {
return value == null ? null : Instant.parse(value);
}
@Named("epochMillis")
public Instant epochMillis(Long value) {
return value == null ? null : Instant.ofEpochMilli(value);
}
}
Mapper:
@Mapper(
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = DateTimeMapper.class
)
public interface EventMapper {
@Mapping(target = "occurredAt", source = "occurredAt", qualifiedByName = "isoInstant")
EventCommand toCommand(EventRequest request);
}
Use qualifiers when:
- multiple conversion methods exist
- same source/target types have different semantic conversions
- boundary-specific conversion is needed
- you want explicit mapping policy
Without qualifier, MapStruct may report ambiguous mapping or choose unintended method depending context.
13. Custom Qualifier Annotation
For stronger typing than string-based @Named:
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface IsoInstant {
}
Mapper method:
public class DateTimeMapper {
@IsoInstant
public Instant parseIsoInstant(String value) {
return value == null ? null : Instant.parse(value);
}
}
Use:
@Mapping(target = "occurredAt", source = "occurredAt", qualifiedBy = IsoInstant.class)
EventCommand toCommand(EventRequest request);
This avoids string typo in qualifiedByName.
14. Collection Mapping
Source/target:
public record Order(
List<OrderLine> lines
) {}
public record OrderResponse(
List<OrderLineResponse> lines
) {}
Mapper:
@Mapper(
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = OrderLineMapper.class
)
public interface OrderMapper {
OrderResponse toResponse(Order order);
}
Line mapper:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface OrderLineMapper {
OrderLineResponse toResponse(OrderLine line);
}
MapStruct generates loop-based mapping.
Conceptually:
List<OrderLineResponse> list = new ArrayList<>(lines.size());
for (OrderLine line : lines) {
list.add(orderLineMapper.toResponse(line));
}
Review:
- null source collection behavior
- mutable vs immutable target list
- collection implementation
- ordering preservation
- nested mapper used
- performance for large collections
15. Set Mapping
Set<TagDto> toDtos(Set<Tag> tags);
Set mapping preserves set semantics, but order may not be stable unless source/target set implementation preserves order.
For stable output, consider List or LinkedHashSet.
Do not expose unordered set in JSON response if consumer expects deterministic order.
16. Map Mapping
Map<String, AttributeDto> toDtoMap(Map<String, Attribute> attributes);
Key/value conversions can be defined:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface AttributeMapper {
Map<String, AttributeDto> toDtoMap(Map<String, Attribute> attributes);
AttributeDto toDto(Attribute attribute);
}
For key conversion:
default String keyToString(AttributeKey key) {
return key == null ? null : key.value();
}
Maps are useful for dynamic attributes, but should have governance:
- max entries
- allowed keys
- value type policy
- key normalization
- order if output matters
- unknown/extension handling
17. Enum Mapping
Same-name enum:
public enum DomainStatus {
PENDING,
APPROVED,
REJECTED
}
public enum ApiStatus {
PENDING,
APPROVED,
REJECTED
}
Mapper:
ApiStatus toApiStatus(DomainStatus status);
Different names:
public enum ProviderStatus {
WAITING,
PAID,
FAIL
}
public enum PaymentStatus {
PENDING,
COMPLETED,
FAILED
}
Mapper:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface PaymentStatusMapper {
@ValueMapping(source = "WAITING", target = "PENDING")
@ValueMapping(source = "PAID", target = "COMPLETED")
@ValueMapping(source = "FAIL", target = "FAILED")
PaymentStatus toDomain(ProviderStatus status);
}
This is exactly where compile-time mapping helps.
18. MappingConstants
For unknown/remaining enum values:
@Mapper
public interface ProviderStatusMapper {
@ValueMapping(source = "WAITING", target = "PENDING")
@ValueMapping(source = "PAID", target = "COMPLETED")
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "UNKNOWN")
PaymentStatus toDomain(ProviderStatus status);
}
or:
@ValueMapping(source = MappingConstants.NULL, target = "UNKNOWN")
Use carefully:
| Strategy | Meaning |
|---|---|
| explicit all values | safest for closed enum |
ANY_REMAINING | map unmapped same-name capable values or leftovers |
ANY_UNMAPPED | catch any unmapped source |
NULL | define null source behavior |
target MappingConstants.NULL | map to null intentionally |
For external provider enum, preserving raw code may be better than collapsing to UNKNOWN.
19. Enum to String
default String statusToCode(PaymentStatus status) {
if (status == null) {
return null;
}
return switch (status) {
case PENDING -> "P";
case COMPLETED -> "C";
case FAILED -> "F";
};
}
String to enum:
default PaymentStatus codeToStatus(String code) {
if (code == null) {
return null;
}
return switch (code.trim().toUpperCase(Locale.ROOT)) {
case "P" -> PaymentStatus.PENDING;
case "C" -> PaymentStatus.COMPLETED;
case "F" -> PaymentStatus.FAILED;
default -> throw new IllegalArgumentException("unknown payment status code: " + code);
};
}
For provider data, consider:
public record ProviderStatusValue(
String raw,
KnownStatus known
) {}
instead of losing raw code.
20. Multiple Source Parameters
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CaseResponseMapper {
@Mapping(target = "caseId", source = "caseRecord.id")
@Mapping(target = "title", source = "caseRecord.title")
@Mapping(target = "assigneeName", source = "assignee.displayName")
@Mapping(target = "queueName", source = "queue.name")
CaseDetailResponse toResponse(
CaseRecord caseRecord,
UserSummary assignee,
QueueSummary queue
);
}
Use when response projection is assembled from multiple sources.
If two sources have same property name and target is ambiguous, specify source explicitly.
21. @BeanMapping
@BeanMapping can configure a mapping method.
Example with explicit ignore by default:
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "caseId", source = "id.value")
@Mapping(target = "title", source = "title")
CaseSummaryResponse toSummary(Case domain);
This is useful for summary projections where you intentionally map only a few fields.
Without ignoreByDefault, unmappedTargetPolicy = ERROR would force all target fields mapped.
Use for:
- summary response
- search result projection
- export row subset
- privacy-safe projection
- partial view
22. Mapping Inheritance
If several methods share mapping config:
@Mapping(target = "customerId", source = "id.value")
@Mapping(target = "fullName", source = "legalName")
CustomerResponse toResponse(Customer customer);
@InheritConfiguration(name = "toResponse")
CustomerSummary toSummary(Customer customer);
Use carefully. Inheritance can reduce repetition but also hide mapping intent.
Prefer it when mapping variants are genuinely aligned.
Avoid if summary/detail fields have different semantic rules.
23. Mapping Nested Object via Helper Method
Instead of many nested target fields:
@Mapper(
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {CustomerMapper.class, MoneyMapper.class}
)
public interface OrderMapper {
OrderResponse toResponse(Order order);
}
Customer mapper:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CustomerMapper {
@Mapping(target = "customerId", source = "id.value")
CustomerSummary toSummary(Customer customer);
}
Target:
public record OrderResponse(
String orderId,
CustomerSummary customer,
MoneyResponse total
) {}
This is often cleaner than flattening everything.
24. Object Factories
If target construction needs factory logic:
public class MoneyFactory {
@ObjectFactory
public Money create(MoneyDto dto) {
if (dto == null) {
return null;
}
return new Money(
new BigDecimal(dto.amount()),
Currency.getInstance(dto.currency())
);
}
}
Use factories when target object creation is non-trivial but still representation-level.
Do not use factory to fetch data from database or make workflow decisions.
Object factories are explored more in Part 025.
25. Implicit Conversion Caution
MapStruct supports many built-in conversions. This is convenient, but not always semantically correct.
Examples to review:
| Conversion | Risk |
|---|---|
String ↔ numeric | leading zero loss, invalid format |
| numeric widening/narrowing | overflow |
| enum ↔ string | external code vs enum name |
| date/time ↔ string | format/timezone ambiguity |
| collection conversion | mutability/order |
| primitive default | null becomes zero/false depending path |
| BigDecimal ↔ String | scale/format |
For boundary-critical values, prefer explicit conversion method.
26. Mapping Tests
26.1 Field Mapping
@Test
void mapsCustomerFields() {
Customer customer = new Customer(new CustomerId("CUS-001"), "Ana");
CustomerResponse response = mapper.toResponse(customer);
assertThat(response.customerId()).isEqualTo("CUS-001");
assertThat(response.fullName()).isEqualTo("Ana");
}
26.2 Nested Mapping
@Test
void mapsNestedCustomerSummary() {
Order order = fixtureOrder();
OrderResponse response = mapper.toResponse(order);
assertThat(response.customer().customerId()).isEqualTo("CUS-001");
}
26.3 Collection Mapping
@Test
void mapsOrderLinesInOrder() {
Order order = fixtureOrderWithLines("A", "B");
OrderResponse response = mapper.toResponse(order);
assertThat(response.lines())
.extracting(OrderLineResponse::sku)
.containsExactly("A", "B");
}
26.4 Enum Mapping Coverage
@Test
void allProviderStatusesMap() {
for (ProviderStatus status : ProviderStatus.values()) {
assertThatCode(() -> mapper.toDomain(status)).doesNotThrowAnyException();
}
}
26.5 Generated Code Inspection
For critical mapper, add code review step:
Open generated mapper.
Verify nested null checks.
Verify enum switch.
Verify collection implementation.
Verify no unexpected default.
27. Production Checklist
Before approving MapStruct mappings:
- Are same-name mappings semantically correct?
- Are different-name mappings explicit?
- Are nested paths intentional?
- Are deep graph traversals avoided?
- Are collection order/mutability expectations tested?
- Are map key/value conversions explicit?
- Are enum mappings explicit for external codes?
- Are constants/defaults truly contract-level, not business decisions?
- Are expressions small and deterministic?
- Are qualifiers used for ambiguous conversion?
- Is
@BeanMapping(ignoreByDefault = true)used for partial projections? - Are all intentional ignores documented?
- Are mapper tests covering semantic fields?
- Is generated code inspected for critical mapping?
28. Anti-Patterns
28.1 Same Name Means Same Meaning
Two fields named status may not have same lifecycle semantics.
28.2 Deep Path Mapping into Lazy Entity Graph
@Mapping(target = "teamName", source = "case.assignee.team.name")
Could trigger unwanted loading and leak graph design.
28.3 Constants for Domain State
Mapper should not approve, reject, close, escalate, or assign.
28.4 Default Value for Missing Critical Data
Defaulting missing amount, currency, id, or timestamp hides data quality problems.
28.5 Expression Dumping Ground
Huge Java expression strings are hard to test and refactor.
28.6 ANY_REMAINING Without Thinking
Unknown enum handling is compatibility policy, not convenience.
29. Mini Case Study: Provider Payment Mapping
Provider DTO:
public record ProviderPaymentDto(
String payment_id,
String amount,
String currency,
String status,
String created_at,
List<ProviderPaymentLineDto> lines
) {}
Command:
public record PaymentImportedCommand(
PaymentId paymentId,
Money money,
ProviderPaymentStatus status,
Instant createdAt,
List<PaymentLineCommand> lines
) {}
Mapper:
@Mapper(
config = CentralMapperConfig.class,
uses = {
PaymentIdMapper.class,
MoneyMapper.class,
DateTimeMapper.class,
PaymentLineMapper.class
}
)
public interface ProviderPaymentMapper {
@Mapping(target = "paymentId", source = "payment_id")
@Mapping(target = "money", source = ".")
@Mapping(target = "status", source = "status", qualifiedByName = "providerStatus")
@Mapping(target = "createdAt", source = "created_at", qualifiedByName = "isoInstant")
PaymentImportedCommand toCommand(ProviderPaymentDto dto);
@Named("providerStatus")
default ProviderPaymentStatus toProviderStatus(String raw) {
if (raw == null) {
return null;
}
return switch (raw.trim().toUpperCase(Locale.ROOT)) {
case "WAITING" -> ProviderPaymentStatus.PENDING;
case "PAID" -> ProviderPaymentStatus.COMPLETED;
case "FAIL" -> ProviderPaymentStatus.FAILED;
default -> ProviderPaymentStatus.UNKNOWN;
};
}
}
Money mapper:
public class MoneyMapper {
public Money toMoney(ProviderPaymentDto dto) {
if (dto == null) {
return null;
}
return new Money(
new BigDecimal(dto.amount()),
Currency.getInstance(dto.currency())
);
}
}
Review:
- provider field names are explicit
- money conversion centralized
- date conversion qualified
- status strategy explicit
- lines delegated
- no business approval logic in mapper
30. Practice Drill
Given:
public record CaseApiRequest(
String caseId,
String title,
String priority,
String reporterUserId,
List<String> tags,
Map<String, String> attributes
) {}
Target:
public record CreateCaseCommand(
CaseId caseId,
String title,
Priority priority,
UserId reporter,
List<Tag> tags,
Map<AttributeKey, AttributeValue> attributes
) {}
Tasks:
- Create mapper with strict target policy.
- Map
caseIdusing value object mapper. - Map
priorityexplicitly. - Map
reporterUserIdtoUserId. - Map
tagstoTag. - Map attribute key/value types.
- Add tests for list order.
- Add tests for unknown priority.
- Add tests for blank attribute key.
- Explain which validations should happen before mapper.
31. Summary
MapStruct’s mapping features are powerful, but their correctness depends on semantic discipline.
Mental model:
Let MapStruct remove mechanical boilerplate, not engineering judgement.
Rules:
- Use convention for safe same-name mappings.
- Use explicit
@Mappingfor semantic clarity. - Keep nested mapping intentional and shallow enough.
- Delegate nested object mapping to helper mappers.
- Test collection order and null behavior.
- Use explicit enum mapping for external statuses.
- Treat constants/defaults as contract decisions.
- Prefer named methods/qualifiers over complex expressions.
- Use
@BeanMapping(ignoreByDefault = true)for summary/projection mappings. - Inspect generated code for critical paths.
Next part deep dives into update, patch, and partial mapping: @MappingTarget, null strategies, merge semantics, dirty intent, and why absence/null/default must be treated as different states.
References
- MapStruct 1.6.3 Reference Guide: https://mapstruct.org/documentation/stable/reference/html/
- MapStruct Stable API Docs: https://mapstruct.org/documentation/stable/api/
- MapStruct Project Home: https://mapstruct.org/
You just completed lesson 23 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.