MapStruct is a popular library for mapping DTOs(Data Transfer Objects) to Entities and vice versa in a Spring Boot application. When dealing with custom mapping and type conversions, MapStruct provides two primary attributes: qualifiedByName with @Mapping and an expression with @Mapping. Each attribute has specific strengths, ideal use cases, and limitations. This topic will help us understand the MapStruct qualifiedByName vs expression attributes of the @Mapping annotation of the MapStruct in a Spring Boot application and provide insight into when to use each for our mapping needs.
What is QualifiedByName in MapStruct?
The qualifiedByName attribute in the @Mapping annotation in MapStruct is used to identify and apply custom mapping methods by name. It allows us to define reusable helper methods that can be applied to specific fields across multiple mappings.
Key Characteristics of QualifiedByName:
- Reusability: With this methods are reusable across multiple mappings, making it easy to use the same logic in different places.
- Type-Safe: Using this attribute methods are defined in the Java class, they provide compile-time safety.
- Better for Complex Logic: Ideal for transformations that are too complex to fit inline but are necessary across different fields or mappings.
What is Expression in MapStruct?
An Expression is an attribute in MapStruct’s @Mapping annotation that allows us to write inline Java code directly within the mapping. It is particularly useful for quick, one-off transformations where defining a separate helper method might not be necessary.
Key Characteristics of Expression:
- In-line Code: The Java expression is embedded directly within the mapping annotation, making it convenient for simple, quick transformations.
- Single-Use: Typically used for one-off transformations specific to a single mapping.
- No Reusability: Expressions are specific to the mapping they are defined in, making them less reusable than qualifiedByName.
Example Use Case for MapStruct QualifiedByName vs Expression
Let’s consider a scenario where we have a WebSeries entity and WebSeriesDTO. We want to map between these classes while handling custom type conversions using both QualifiedByName and Expression.
Entity: WebSeries
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
@Data
@Entity
public class WebSeries {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int release;
}
DTO: WebSeriesDTO
import lombok.Data;
@Data
public class WebSeriesDTO {
private Long id;
private String name;
private String release;
}
Using QualifiedByName for Reusable Conversions
Let’s consider that we need to convert the String type DTO’s release field to the Int type Entity’s release field and vice versa. Since these conversions might require us to use them in multiple places, instead of defining them as separate,we can reuse these methods by using the qualifiedByName attribute of MapStruct.
Use QualifiedByName in the Mapper
We need to create an Interface with the @Mapper annotation and then convert helper methods annotated with the @Name annotation. After that, we need to define mapping methods and use the qualifiedByName attribute of the @Mapping annotation.
Mapper Interface: WebSeriesMapper
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
@Mapper
public interface WebSeriesMapper {
WebSeriesMapper INSTANCE = Mappers.getMapper(WebSeriesMapper.class);
//Custom method to map int to String
@Named("mapIntToString")
default String mapStatusToBoolean(int release) {
return release == 1 ? "yes": "no";
}
//Custom method to map String to int
@Named("mapStringToInt")
default int mapStatusToInt(String release) {
return release.equals("yes") ? 1 : 0;
}
//Map Entity to DTO with custom type handling
@Mapping(source = "release", target = "release", qualifiedByName = "mapIntToString")
WebSeriesDTO entityToDto(WebSeries entity);
//Map DTO to Entity with custom type handling
@Mapping(source = "release", target = "release", qualifiedByName = "mapStringToInt")
WebSeries dtoToEntity(WebSeriesDTO dto);
}
Using Expression for One-Off Transformations
For a single-use conversion, such as converting the release field from boolean to int and vice versa, the expression attribute of MapStruct is more convenient. Let’s consider the example below:
Entity: WebSeries
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
@Data
@Entity
public class WebSeries {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int release;
}
DTO: WebSeriesDTO
import lombok.Data;
@Data
public class WebSeriesDTO {
private Long id;
private String name;
private boolean release;
}
Using Expression in Mapper
We are using the expression attribute of MapStruct by using @Mapping annotation in the Mapper interface.
Mapper Interface: WebSeriesMapper
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface WebSeriesMapper {
WebSeriesMapper INSTANCE = Mappers.getMapper(WebSeriesMapper.class);
//Map Entity to DTO with custom type handling
@Mapping(target = "release", expression = "java(entity.getRelease()==1 ? true : false)")
WebSeriesDTO entityToDto(WebSeries entity);
//Map DTO to Entity with custom type handling
@Mapping(target = "release", expression = "java(dto.isRelease() ? 1 : 0)")
WebSeries dtoToEntity(WebSeriesDTO dto);
}
Comparison: QualifiedByName vs Expression
Feature | QualifiedByName | Expression |
Use Case | Complex, reusable conversions | Simple, single-use transformations |
Reusability | High – Can be applied across multiple mappings | Low – Embedded directly in a single mapping |
Type Safety | High – Uses type-safe, named methods | Moderate – Java code is embedded directly |
Complex Logic | Suitable for detailed or complex logic | Limited to simple logic |
Code Maintenance | Centralized in reusable methods | Harder to maintain if repeated in multiple places |
Performance | Better for repeated logic due to compile-time resolution | Slightly slower, evaluated within each mapping |
When to Use Each Approach
- Use the qualifiedByName attribute for reusable, complex mappings that can be applied to multiple fields or mapping interfaces. It keeps your code modular and easier to maintain.
- Use expression attribute for quick, one-off transformations that don’t justify a dedicated helper method. This is ideal for fields that require simple inline logic without being reused elsewhere.
Conclusion
Both QualifiedByName and Expression attributes have their unique advantages in MapStruct:
- The qualifiedByName attribute is excellent for modular, reusable mapping methods, ensuring clean code and easy maintenance.
- The expression attribute works well for straightforward, one-off transformations where the mapping logic is concise.