A lightweight KSP (Kotlin Symbol Processing) processor that generates mapper implementations for Jimmer entities.
Jimmer entities are immutable interfaces that use a DSL builder pattern. Standard mapping libraries like MapStruct cannot generate code for them because they rely on setters. This library bridges that gap by generating Jimmer DSL code at compile time from annotated mapper interfaces.
Add the JitPack repository and dependencies:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "2.1.10-1.0.31"
}
dependencies {
implementation("com.github.sleepkqq.jimmer-mapper-kt:jimmer-mapper-kt-annotations:1.0.0")
ksp("com.github.sleepkqq.jimmer-mapper-kt:jimmer-mapper-kt-processor:1.0.0")
}// settings.gradle.kts
includeBuild("../jimmer-mapper-kt")Define a mapper interface annotated with @JimmerMapper. The processor matches source properties to target entity properties by name.
data class CreateBookInput(
val title: String,
val isbn: String,
val pageCount: Int?,
)
@JimmerMapper
interface BookMapper {
fun toNew(input: CreateBookInput): Book
}Generated:
@ApplicationScoped
class BookMapperImpl : BookMapper {
override fun toNew(input: CreateBookInput): Book = Book {
title = input.title
isbn = input.isbn
pageCount = input.pageCount
}
}Parameters named {entityProperty}Id are automatically mapped to FK shorthand properties on the Jimmer Draft:
@JimmerMapper
interface BookMapper {
fun toNew(title: String, authorId: UUID): Book
}Generated:
override fun toNew(title: String, authorId: UUID): Book = Book {
this.title = title
this.authorId = authorId
}Use @Base to mark a parameter as the existing entity. The generated code uses Jimmer's copy DSL:
@JimmerMapper
interface BookMapper {
fun toUpdated(@Base existing: Book, title: String, pageCount: Int): Book
}Generated:
override fun toUpdated(existing: Book, title: String, pageCount: Int): Book = Book(existing) {
this.title = title
this.pageCount = pageCount
}Override auto-matching with explicit source-to-target mapping:
data class ImportBookInput(
val bookTitle: String,
val cover: String?,
)
@JimmerMapper
interface BookMapper {
@Mapping(source = "input.bookTitle", target = "title")
@Mapping(source = "input.cover", target = "avatarKey")
fun toNew(input: ImportBookInput): Book
}Skip specific target properties:
@JimmerMapper
interface BookMapper {
@IgnoreMapping("reviews", "ratings")
fun toNew(input: CreateBookInput): Book
}When a target property is a @ManyToOne / @OneToOne Jimmer entity and the source has matching scalar fields, the processor generates a nested Jimmer DSL block:
// Target entity Book has: val publisher: Publisher (@ManyToOne)
// Publisher has: val name: String, val country: String
data class BookEntry(
val title: String,
val name: String, // matches Publisher.name
val country: String, // matches Publisher.country
)
@JimmerMapper
interface BookMapper {
fun toNew(entry: BookEntry): Book
}Generated:
override fun toNew(entry: BookEntry): Book = Book {
title = entry.title
publisher = Publisher {
name = entry.name
country = entry.country
}
}When a target has a @OneToMany list and the source has a matching collection, the processor looks for a sibling method in the same mapper interface that maps the element type:
data class BookEntry(
val title: String,
val chapters: List<ChapterEntry>,
)
data class ChapterEntry(
val title: String,
val pageCount: Int,
)
@JimmerMapper
interface BookMapper {
fun toNew(entry: BookEntry): Book
// Sibling method — used automatically for chapters mapping
fun toChapter(entry: ChapterEntry): Chapter
}The processor discovers toChapter and generates chapters = entry.chapters.map { toChapter(it) }.
When updating an entity, list parameters are merged with the existing collection:
@JimmerMapper
interface BookMapper {
fun toUpdated(@Base existing: Book, chapters: List<Chapter>): Book
}Generated:
override fun toUpdated(existing: Book, chapters: List<Chapter>): Book =
Book(existing) {
this.chapters = existing.chapters + chapters
}The processor automatically skips:
@Id+@GeneratedValueproperties (auto-generated by DB)@OneToMany/@ManyToManyproperties (unless explicitly mapped or sibling method found)@ManyToOne/@OneToOneproperties without FK parameter or matching source (parent set by Jimmer on save)@MappedSuperclassproperties (version,createdAt,updatedAt)- Nullable properties without a source (default to
null)
| Option | Values | Default | Description |
|---|---|---|---|
jimmerMapper.framework |
quarkus, spring, singleton, none |
quarkus |
Framework annotation on generated classes |
jimmerMapper.nativeImage |
true, false |
false |
Generate reflect-config.json for GraalVM native image |
| Framework | Generated annotation |
|---|---|
quarkus |
@ApplicationScoped (jakarta.enterprise.context) |
spring |
@Component (org.springframework.stereotype) |
singleton |
@Singleton (jakarta.inject) |
none |
No annotation |
ksp {
arg("jimmerMapper.framework", "spring")
arg("jimmerMapper.nativeImage", "true")
}When nativeImage is enabled, the processor generates META-INF/native-image/com.sleepkqq/jimmer-mapper-kt/reflect-config.json with all generated mapper classes registered for reflection.
- Kotlin 2.1+
- KSP 2.1+
- Jimmer 0.9+
- JVM 21+
Apache License 2.0