Articles
MapStruct Interview Questions: Generated Mappers, Null Strategies, and Update Methods
Practice MapStruct interview questions on generated Java, null strategies, create versus update mappings, PATCH presence, and collection identity safety.
A MapStruct interview often turns on one deceptively small question: when the source field is null, does the target keep its old value, become null, or receive a default? The answer depends on the mapping method, its configuration, and the data contract. “MapStruct handles nulls” is too broad to defend in a code review.
Predict the generated Java, then connect it to the intended create or update operation. This article uses official facts from the stable MapStruct documentation, original exercises, and clearly identified engineering inference. It does not claim candidate-reported interview frequency.
What does MapStruct generate, and why does that matter?
Official fact: MapStruct is an annotation processor that generates type-safe mapping implementations at compile time. Its stable reference currently identifies version 1.6.3. The generated methods ordinarily use Java accessors and conversion methods rather than runtime reflection. See the reference guide.
Original exercise: A mapper receives CustomerDto and creates Customer. Both have a name, while the entity additionally has an identifier and a server-managed creation timestamp. Decide which fields the client may control before adding annotations.
A mapper can compile while copying fields the client should not control. Copying a supplied identifier into an existing entity might change an association or confuse an ORM identity. Mapping a client timestamp into an audit field might erase evidence. These are contract defects, not annotation-processor failures.
Engineering inference: review the generated implementation whenever a surprising branch affects persisted data. You can inspect whether it invokes a setter, creates a nested object, delegates to another mapper, or returns early. The annotation is a concise declaration; the generated statements reveal the actual operation.
Do not edit the generated file to repair behavior. Change the mapper configuration or an explicitly maintained helper, rebuild, and verify the new output. A clean build also prevents a stale implementation from making an apparently missing annotation change look ineffective.
Which of the three null strategies answers the question?
These similarly named settings operate at different levels. Start by asking whether the null is the entire source parameter or one property inside a non-null source.
Official fact: NullValueMappingStrategy controls null arguments to mapping methods. The default returns null; RETURN_DEFAULT can produce an empty collection, empty map, or instantiated bean, depending on the mapping kind. Expressions must handle null themselves when relevant. See its API contract.
Official fact: NullValuePropertyMappingStrategy applies to update methods with @MappingTarget. IGNORE preserves the existing target property, SET_TO_NULL clears it, and SET_TO_DEFAULT uses an appropriate default. More specific mapping configuration overrides broader configuration. See the property strategy API.
Official fact: NullValueCheckStrategy concerns generated source null checks. It does not define whether an omitted JSON field represents “leave unchanged” or “clear.” Presence checkers and particular collection accessors affect those checks. See the check strategy API.
| Interview question | Configuration to inspect | What it does not decide |
|---|---|---|
What happens for map(null)? | Null argument mapping strategy | Meaning of an absent HTTP field |
| Does a null field erase an existing property? | Update property strategy | Concurrency with another writer |
| Is a null guard generated before conversion? | Null check strategy | The business meaning of clearing |
Original exercise: Start with target name Ada. Map a non-null DTO whose name is null through three otherwise identical update methods. Predict Ada for IGNORE, null for SET_TO_NULL, and the appropriate default for SET_TO_DEFAULT. Now change the experiment to a create method. You must reconsider the answer: the update-property strategy is not a general create-mapping default policy.
Can IGNORE implement a complete PATCH contract?
Suppose the endpoint contract says absent means preserve, explicit null means clear, and a supplied string means replace. A plain nullable Java field can collapse the first two states during decoding.
Original exercise: Existing display name is Ada. The following requests must have different results:
| Request state | Desired target | Information needed |
|---|---|---|
| Field absent | Ada | Presence is false |
| Field present as null | null | Presence is true, value is null |
Field present as Lin | Lin | Presence is true, value is Lin |
Configuring IGNORE on the nullable field keeps Ada for both absent and explicit null. Configuring SET_TO_NULL can clear both. Neither setting can recover information the DTO no longer carries.
Engineering inference: represent presence explicitly at the API boundary. A wrapper or DTO with both a presence flag and nullable value can support the contract. Then make the clearing branch explicit rather than expecting a global strategy to infer it:
// Original domain logic; not generated MapStruct output.
if (patch.isDisplayNamePresent()) {
target.setDisplayName(patch.getDisplayName());
}
A source presence checker such as hasDisplayName() is another supported mechanism to investigate, but inspect its generated branch with the selected null strategies. A presence checker is not automatically the same as a three-state PATCH representation.
Separate transport decoding tests from mapper tests. First prove absent and explicit null remain distinguishable. Then prove their mapping outcomes. A mapper-only fixture that manually constructs two distinct inputs cannot establish that the actual JSON decoder preserves those states.
What does an update method actually mutate?
Official fact: @MappingTarget identifies the existing target; it must not be null. An update method can return void or return the mapped target, subject to the documented signature rules. See MappingTarget.
Original exercise: Compare a create method with this update declaration:
@Mapper
interface CustomerMapper {
Customer create(CustomerDto source);
@BeanMapping(
nullValuePropertyMappingStrategy =
NullValuePropertyMappingStrategy.IGNORE)
void update(CustomerDto source, @MappingTarget Customer target);
}
Assume ordinary bean getters and setters. A representative update branch for a nullable name is:
// Simplified illustration: inspect actual generated code.
if (source == null) return;
if (source.getName() != null) {
target.setName(source.getName());
}
The first guard concerns the whole source. The second concerns one property. Returning early for a null source is different from iterating over all target properties and clearing them.
Engineering inference: supply a managed target deliberately if the operation is meant to mutate an existing persistence entity. Creating a new entity with the same identifier is a different persistence operation. MapStruct itself does not supply transaction isolation, optimistic locking, or authorization.
An interview follow-up might introduce two concurrent updates. IGNORE only decides what one mapper invocation assigns. It cannot prevent a stale target from overwriting a newer version during persistence. Explain the mapper branch first, then the repository's version-check or transaction policy.
Will a collection update merge items by identity?
Official fact: collection mapping behavior depends on available setters, getters, adders, and CollectionMappingStrategy. Some update paths clear and repopulate collections; getter or adder paths receive source null checks even when other null strategies suggest different behavior. TARGET_IMMUTABLE changes the update accessor treatment. See collection strategies.
Original exercise: Existing order lines are A and B. The DTO supplies B with a new quantity and C as a new line. Before predicting the resulting collection, ask whether omission of A means deletion, preservation, or rejection. Also ask whether B must keep its entity identity.
A generated loop that constructs mapped items and adds them is not automatically an identity-aware reconciliation algorithm. Even a final collection containing B and C can be wrong if B's original managed entity was discarded and replaced with a fresh one.
Engineering inference: for aggregate updates, define matching, deletion, duplicate-ID handling, and child ownership outside the generic property copy. One possible domain algorithm indexes existing lines by ID, updates matching children, creates approved new children, and removes omitted children only when the endpoint contract allows it.
Then test A's fate, B's identity, C's creation, and duplicate input identifiers independently. “List size is two” is insufficient evidence: two entirely new objects could produce the same count while breaking relationships.
What would you inspect when a mapper is missing or ambiguous?
A missing generated implementation calls for build evidence: check annotation processing, processor dependencies, source generation, and whether the IDE and command-line build agree. A clean command-line build is a stronger discriminator than repeatedly restarting the application.
For Spring integration, distinguish component discovery from construction. A generated mapper using the Spring component model is a bean; a manually obtained mapper instance follows another ownership path. If it delegates to other mappers or helpers, their injection and lifecycle must agree.
For an ambiguous conversion, ask which method is intended. Two helpers that both convert the same source type into the same target type can require an explicit qualifier. Do not choose a helper by accidental declaration order.
Engineering inference: prefer a failing build for newly unmapped target fields in sensitive contracts when your team's policy supports it. An added server-managed property should prompt a deliberate ignore or mapping decision. A silent mapping is convenient only when the omission is acceptable.
What should the tests prove?
Use a matrix with whole-source null, null property, ordinary value, and existing non-default target values. An update test starting from an empty target often cannot distinguish preservation from clearing.
Include custom conversion behavior. If a helper intentionally handles null, generated guards may determine whether it is invoked. Verify the actual method rather than assuming the global null setting reaches every nested conversion.
For PATCH, test the transport and mapping together at least once. For collections, test identity as well as values. For persistence, keep concurrency tests separate from mapper semantics so a transaction failure does not disguise a mapping defect.
How would you review a generated-code diff?
Original exercise: A new nested address field causes the generated mapper to create a target address instead of leaving the existing one unchanged. The top-level customer name still maps correctly, so a happy-path snapshot passes. Review the nested branch and ask whether replacing the address is allowed for this operation.
Engineering inference: focus the diff review on assignments with domain consequences: identifiers, audit fields, nested object construction, custom conversions, and collection clearing. Counted lines of generated code do not measure correctness. A five-line branch that erases an association matters more than a hundred routine getter-to-setter calls.
Record the intended result for an existing non-empty address, a missing source address, and a supplied replacement. If those expectations differ between create and update, keep distinct methods or explicit configuration rather than forcing one mapper method to serve both contracts.