Skip to content

Repository files navigation

KMapper

A Kotlin compiler plugin that automatically generates mapping methods between data classes using a fluent DSL.

Overview

KMapper is a Kotlin compiler plugin that provides code generation capabilities for mapping between data classes. It uses a fluent DSL syntax with the mapper extension function to transform objects from one type to another, with compile-time validation to ensure all required constructor parameters are mapped.

Features

  • Kotlin 2.0+ Support: Built with K2 compiler support (Kotlin 2.4.0)
  • Fluent DSL: Intuitive assignment-based mapping syntax with property = value
  • Compile-time Validation: Ensures all required constructor parameters are mapped
  • IR-Based Generation: Uses Kotlin's IR (Intermediate Representation) for robust code generation
  • Symmetric Enum Mapping: When source and target enum entries share the same names, the plugin automatically maps them without additional configuration

Requirements

  • Kotlin 2.4.0 or later
  • JVM 17+
  • Gradle build system

Installation

Using Gradle

Add the plugin to your project's build.gradle.kts:

build.gradle.kts

plugins {
kotlin("jvm") version "2.4.0"
id("community.flock.kmapper") version "0.0.0-SNAPSHOT"
}

settings.gradle.kts

pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
mavenLocal()
// maven(url="https://central.sonatype.com/repository/maven-snapshots/")
}
}

Using Maven

Load the KMapper Maven integration by adding it as a dependency of kotlin-maven-plugin. The extension will:

  • Auto-register the KMapper Kotlin compiler plugin (transitively on the plugin classpath)
  • Ensure the runtime library (compiler-runtime) is on your project compile classpath

Kotlin version used/tested: 2.4.0.

Minimal setup:

<dependencies>
<dependency>
<groupId>community.flock.kmapper</groupId>
<artifactId>compiler-runtime</artifactId>
<version>0.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>2.4.0</version>
...(other plugin configuration)
<dependencies>
<dependency>
<groupId>community.flock.kmapper</groupId>
<artifactId>compiler-plugin</artifactId>
<version>0.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

Troubleshooting:

  • Ensure kotlin-maven-plugin version is 2.4.0 (matching our tested Kotlin version).
  • Make sure the KMapper maven-plugin dependency is placed under kotlin-maven-plugin’s (not in the project section).
  • In multi-module builds, add the kotlin-maven-plugin configuration in each module that compiles Kotlin (you can use in the parent for reuse).
  • You can set a property <kmapper.version>0.0.0-SNAPSHOT</kmapper.version> and the extension will use it to resolve the runtime version if needed.

Usage

Basic Example

  1. Import the mapper functions:
importcommunity.flock.kmapper.mapperimportcommunity.flock.kmapper.ignore
  1. Define your data classes:
data classUser(
valid:Int, valfirstName:String, vallastName:String, valage:Int, valactive:Boolean
)
data classUserDto(
valid:Int,
valname:String,
valage:String,
valactive:Boolean = false
)
  1. Use the mapper DSL to transform objects:
funmain() {
val user =User(1, "John", "Doe", 99, true)
val userDto:UserDto= user.mapper {
age = it.age.toString()
name ="${it.firstName}${it.lastName}"
active.ignore()
}
println(userDto) // Output: UserDto(name=John Doe, age=99, active=false)
}

Symmetric Enum Mapping

When your source and target enums have the same entry names, KMapper will map them automatically by name at compile time—no manual conversion needed.

Example:

enumclassStatus { NEW, ACTIVE, SUSPENDED }
data classUser (valname:String, valstatus:Status)
enumclassStatusDto { NEW, ACTIVE, SUSPENDED }
data classUserDto (valname:String, valstatus:StatusDto)
funmain() {
val user =User(name ="John Doe", status =Status.ACTIVE)
val dto:UserDto= user.mapper { }
println(dto) // UserDto(name=John Doe, status=ACTIVE)
}

Auto-Mapping Identical Classes

When source and target classes share the same property names and types, no lambda is needed:

data classUser(valid:Int, valname:String, valage:Int)
data classUserDto(valid:Int, valname:String, valage:Int)
val user =User(id =1, name ="John Doe", age =99)
val dto:UserDto= user.mapper()

Nested Object Mapping

KMapper automatically maps nested data classes when their properties match:

data classAddress(valstreet:String, valcity:String)
data classPerson(valname:String, valaddress:Address)
data classAddressDto(valstreet:String, valcity:String)
data classPersonDto(valname:String, valaddress:AddressDto)
val person =Person("John Doe", Address("Main Street", "Hamburg"))
val dto:PersonDto= person.mapper()
// PersonDto(name=John Doe, address=AddressDto(street=Main Street, city=Hamburg))

Value Class Mapping

Value classes are supported when the source and target value classes wrap the same type:

@JvmInline value classId(valid:Int)
data classUser(valid:Id, valname:String)
@JvmInline value classIdDto(valid:Int)
data classUserDto(valid:IdDto, valname:String)
val user =User(id =Id(1), name ="John Doe")
val dto:UserDto= user.mapper()
// UserDto(id=IdDto(id=1), name=John Doe)

KMapper also supports automatic unwrapping and wrapping of value classes:

// Unwrap: value class → primitive
@JvmInline value classId(valid:Int)
data classUser(valid:Id, valname:String)
data classUserDto(valid:Int, valname:String)
val dto:UserDto=User(Id(1), "John Doe").mapper()
// UserDto(id=1, name=John Doe)// Wrap: primitive → value classdata classSource(valid:Int, valname:String)
data classTarget(valid:Id, valname:String)
val target:Target=Source(42, "test").mapper()
// Target(id=Id(id=42), name=test)

Numeric Widening

KMapper implicitly widens numeric types when mapping, following JVM's standard widening conversions:

SourceAllowed Targets
ByteShort, Int, Long, Float, Double
ShortInt, Long, Float, Double
IntLong, Float, Double
LongFloat, Double
FloatDouble
data classSource(valvalue:Int, valname:String)
data classTarget(valvalue:Long, valname:String)
val target:Target=Source(42, "test").mapper()
// Target(value=42, name=test)

Narrowing conversions (e.g., LongInt) are not allowed and will produce a compile-time error.

List Mapping

Lists of primitives are mapped automatically when types match. Lists of data classes are mapped recursively:

data classAccount(valname:String)
data classUser(valid:Int, valaccounts:List<Account>)
data classAccountDto(valname:String)
data classUserDto(valid:Int, valaccounts:List<AccountDto>)
val user =User(id =1, accounts =listOf(Account("John Doe")))
val dto:UserDto= user.mapper()
// UserDto(id=1, accounts=[Account(name=John Doe)])

Nullable Fields

KMapper handles nullability: a non-nullable source can map to a nullable target, but not the other way around. Nullable targets without a matching source can be explicitly set:

data classPerson(valfirstName:String)
data classPersonDto(valfirstName:String, vallastName:String?)
val dto:PersonDto=Person("John").mapper {
lastName =null
}
// PersonDto(firstName=John, lastName=null)

Default Values

Target parameters with default values don't require a mapping when no matching source property exists:

data classPerson(valfirstName:String)
data classPersonDto(valfirstName:String, vallastName:String = "Doe")
val dto:PersonDto=Person("John").mapper()
// PersonDto(firstName=John, lastName=Doe)

Ignoring Fields

Use ignore() to skip auto-mapping for a field, falling back to its default value:

data classPerson(valfirstName:String)
data classPersonDto(valfirstName:String = "HELLO")
val dto:PersonDto=Person("John").mapper {
firstName.ignore()
}
// PersonDto(firstName=HELLO)

Generated Code

The plugin automatically generates the mapping implementation at compile time, replacing the mapper function call with the actual object construction code.

Performance

Because KMapper is a compile-time codegen plugin (no reflection, no runtime registry), the generated code is equivalent to a hand-written constructor call and runtime overhead is essentially zero.

This is verified by JMH benchmarks in benchmarks/, which compare mapper { ... } against hand-written equivalents for both a flat 3-field mapping and a nested mapping with value classes, lists, enums, and numeric widening. The CI pipeline runs the suite on every PR and fails the build if a kmapper benchmark is more than 5× slower than its manual counterpart, guarding against accidental regressions.

Notes:

  • Symmetric enum mapping triggers when the target constructor parameter type is an enum and there is a source value of another enum with the same entry names.
  • If names differ or you need custom mapping, you can still provide an explicit mapping expression: status = when(it.status){ SourceStatus.NEW -> TargetStatus.NEW /* ... */ }.

IDE Support

The K2 Kotlin IntelliJ plugin supports running third party FIR plugins in the IDE, but this feature is hidden behind a flag.

To enable it, do the following:

  • Enable K2 Mode for the Kotlin IntelliJ plugin.
  • Open the Registry
  • Set the kotlin.k2.only.bundled.compiler.plugins.enabled entry to false.

About

Kotlin object mapper

Resources

Stars

22 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages