When working with MapStruct, it’s common to define small helper methods directly within your mappers and invoke them using @Mapping(expression = "..."). While convenient, this practice can lead to an unexpected side effect: MapStruct might automatically apply these generic-looking helper methods to other fields of the same type throughout your mapper, even if you only intended them for a specific transformation.
This article will explore this common pitfall and provide effective strategies to maintain explicit control over your mapping logic, ensuring helper methods are used exactly where you intend them.
The Scenario: A Seemingly Simple Helper
Let’s consider a practical example. Imagine you have a BalrogDto and a Balrog model. Your goal is to uppercase only the trueName field during the mapping process, leaving the battleName field as is.
DTO and Model Structure:
class BalrogDto(
val millenniaOld: Int,
val trueName: String,
val battleName: String,
)
class Balrog(
val millenniaOld: Int,
val trueName: String,
val battleName: String,
)
Initial Mapper Implementation:
@Mapper(componentModel = "spring")
abstract class BalrogMapper {
@Mapping(target = "trueName", expression = "java(uppercased(dto.getTrueName()))")
abstract fun toModel(dto: BalrogDto): Balrog
protected fun uppercased(value: String): String = value.uppercase()
}
Here, we’ve explicitly instructed MapStruct to use the uppercased helper method for the trueName field via an expression. Our expectation is that battleName would be mapped without any alteration.
The Unintended Consequence
However, if you examine the MapStruct-generated implementation of BalrogMapperImpl, you might find something unexpected:
@Generated(...)
@Component
public class BalrogMapperImpl extends BalrogMapper {
@Override
public Balrog toModel(BalrogDto dto) {
// ... other boilerplate code ...
// Both trueName and battleName are inadvertently uppercased!
String battleName = uppercased(dto.getBattleName());
String trueName = uppercased(dto.getTrueName());
Balrog balrog = new Balrog(millenniaOld, trueName, battleName);
return balrog;
}
}
As you can see, the battleName field has also been transformed by the uppercased method, which goes against our original intention.
Why MapStruct Behaves This Way
This behavior stems from how MapStruct discovers and applies mapping methods. It primarily relies on method signatures. A protected String -> String method within your @Mapper is recognized as a general-purpose mapping candidate. Consequently, when MapStruct encounters other String-to-String mappings (such as for battleName), it can automatically discover and apply this seemingly suitable helper method.
It’s crucial to understand that using a helper method within an expression for a specific target does not restrict its discoverability for other fields of the same type within the same mapper.
Solutions: Regaining Control Over Helper Methods
Here are effective strategies to prevent MapStruct from unexpectedly reusing your helper methods, giving you precise control over your mapping transformations:
1. Externalize the Helper Method
The simplest solution is to move your helper logic into a separate utility class. MapStruct will not scan this external class as a source of general mapping methods.
Utility Class Example:
object StringUtils {
@JvmStatic // Essential for direct Java static method calls in expressions
fun uppercased(s: String): String = s.uppercase()
}
Revised Mapper:
@Mapper(componentModel = "spring", imports = [StringUtils::class])
abstract class BalrogMapper {
@Mapping(target = "trueName", expression = "java(StringUtils.uppercased(dto.getTrueName()))")
abstract fun toModel(dto: BalrogDto): Balrog
}
By explicitly importing StringUtils and referencing its method in the expression, you ensure that MapStruct only uses this helper when directly instructed. It won’t be auto-applied as it’s not part of the mapper’s internal mapping method discovery.
2. Qualify and Isolate with @Named
If you prefer to keep your helper method within the mapper class, you can qualify it using the @org.mapstruct.Named annotation. You then explicitly reference it via qualifiedByName in your @Mapping annotation.
Revised Mapper:
@Mapper(componentModel = "spring")
abstract class BalrogMapper {
@Mapping(target = "trueName", qualifiedByName = ["uppercased"])
abstract fun toModel(dto: BalrogDto): Balrog
@Named("uppercased")
protected fun uppercased(value: String): String = value.uppercase()
}
This approach clearly tells MapStruct to use the uppercased method only when it’s explicitly called by its designated name, preventing accidental global application.
3. Make the Transformation Type-Specific
For situations where the transformation represents a distinct domain concept, consider introducing a new, specific type. This eliminates the generic String -> String method signature that MapStruct might otherwise auto-apply.
Updated Model and New Type:
class Balrog(
val millenniaOld: Int,
val trueName: TrueName, // Now uses a specific type
val battleName: String,
)
class TrueName(val value: String) // A dedicated type for trueName
Revised Mapper:
@Mapper(componentModel = "spring")
abstract class BalrogMapper {
abstract fun toModel(dto: BalrogDto): Balrog
// The method now maps String -> TrueName, making it unique
fun toTrueName(raw: String): TrueName = TrueName(raw.uppercase())
}
With this change, MapStruct will not mistakenly apply toTrueName to other String fields because its signature is now unique (String to TrueName), rather than a generic String to String. This option is particularly beneficial when your domain model can benefit from richer, more specific types.
Key Takeaways
- Method Signature is Key: MapStruct heavily relies on method signatures for auto-application. Generic helper methods like
String -> Stringwithin your@Mapperare susceptible to being reused across multiple fields. expressionDoes Not Limit Scope: Merely using a helper method within anexpressionfor a particular target does not restrict MapStruct from discovering and applying it to other fields with compatible types.- Prioritize Explicit Control: To ensure helper methods are used only where intended, either move them into external utility classes or leverage
@Namedin conjunction withqualifiedByName. - Embrace Type Safety: When appropriate, introduce specific domain types for transformations. This makes your mapping methods inherently type-specific and significantly reduces the risk of unintended reuse.
By understanding these nuances and applying the recommended strategies, you can effectively leverage MapStruct’s powerful features while maintaining precise and predictable control over your data transformations.