MapStruct
Module allows you to integrate the MapStruct library to convert classes between each other.
Dependency¶
The Kora integration is a compile-time extension that is auto-activated as soon as the mapstruct-processor is on the
annotation-processor classpath — no extra Kora artifact or module import is required.
Dependency build.gradle:
MapStruct in Kotlin works with kapt, so you are required to configure kapt plugin build.gradle.kts:
The latest working version for kapt + ksp is 1.9.10-1.0.13, in later versions of KSP the compatibility between the two tools is broken at the Gradle Plugin level.
You need to allow the output of kapt to be used as input for KSP build.gradle.kts:
ksp {
allowSourcesFromOtherPlugins = true
}
tasks.withType<KspTask> {
dependsOn(tasks.named("kaptGenerateStubsKotlin").get())
dependsOn(tasks.named("kaptKotlin").get())
}
Successful building of the application may be only on second try, this is a KSP behavior.
Dependency build.gradle.kts:
Usage¶
The creation of the mappers themselves falls to the MapStruct library; Kora only contributes a compile-time extension that makes the generated mappers available in the dependency container.
The extension is registered automatically through ServiceLoader and activates as soon as the org.mapstruct.Mapper
annotation is present on the classpath (an annotation-processor extension for Java, a KSP extension for Kotlin). For every
requested @Mapper interface or abstract class it locates the MapStruct-generated <Mapper>Impl in the same package and
exposes its public constructor as a component. Because of this you do not need any Kora module or configuration, and you
do not need componentModel = "kora" — the default componentModel works out of the box.
Declare a mapper the standard MapStruct way and it becomes injectable:
enum class CarType { TYPE1, TYPE2 }
data class Car(val make: String, val numberOfSeats: Int, val type: CarType)
data class CarDto(val make: String, val seatCount: Int, val type: String)
@Mapper
interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
fun map(car: Car): CarDto
}
@Mapper is supported both on interfaces and on abstract classes, and on mappers nested inside an enclosing type — in the
nested case the extension resolves the generated implementation by joining the enclosing names with $
(for example SomeInterface.CarMapper becomes SomeInterface$CarMapperImpl).
Usage in a service¶
An injected mapper is an ordinary Kora component, so you constructor-inject it into a @Component service like any other dependency:
Mapper dependencies¶
A mapper often delegates to helper mappers or services. MapStruct wires those helpers through the uses attribute of
@Mapper. To have Kora supply them from the dependency container (rather than MapStruct instantiating them itself), generate
the implementation with constructor injection: set injectionStrategy = InjectionStrategy.CONSTRUCTOR and
componentModel = "jakarta". The generated <Mapper>Impl then receives every uses type through its public constructor, and
Kora resolves each of them from the graph — so the helper must be available as a component (for example annotated with
@Component or provided by a factory).
@Component
public final class DateMapper {
public String asString(Date date) {
return date != null ? new SimpleDateFormat("yyyy-MM-dd").format(date) : null;
}
public Date asDate(String date) throws ParseException {
return date != null ? new SimpleDateFormat("yyyy-MM-dd").parse(date) : null;
}
}
@Mapper(uses = DateMapper.class,
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
componentModel = "jakarta")
public interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto map(Car car);
}
@Component
class DateMapper {
fun asString(date: Date?): String? =
date?.let { SimpleDateFormat("yyyy-MM-dd").format(it) }
fun asDate(date: String?): Date? =
date?.let { SimpleDateFormat("yyyy-MM-dd").parse(it) }
}
@Mapper(uses = [DateMapper::class],
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
componentModel = "jakarta")
interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
fun map(car: Car): CarDto
}
Tag¶
A @Mapper may be qualified with a @Tag, and the extension provides the mapper only when the requested
tags equal the tags declared on the mapper type. This lets you register several mappers of the same type and disambiguate them
at the injection point: