KMapper
KMapper copied to clipboard
Object to Object mapper Libraly for Kotlin.
KMapper
KMapper is a object to object mapper library for Kotlin, which provides the following features.
Bean mappingwithObjects,Map, andPairas sources- Flexible and safe mapping based on function calls with reflection.
- Richer features and thus more flexible and labor-saving mapping.
A brief benchmark result is posted in the following repository.
Demo code
Here is a comparison between writing the mapping code by manually and using KMapper.
If you write it manually, the more arguments you have, the more complicated the description will be.
However, by using KMapper, you can perform mapping without writing much code.
Also, no external configuration file is required.
// If you write manually.
val dst = Dst(
param1 = src.param1,
param2 = src.param2,
param3 = src.param3,
param4 = src.param4,
param5 = src.param5,
...
)
// If you use KMapper
val dst = KMapper(::Dst).map(src)
You can specify not only one source, but also multiple objects, Pair, Map, etc.
val dst = KMapper(::Dst).map(
"param1" to "value of param1",
mapOf("param2" to 1, "param3" to 2L),
src1,
src2
)
Installation
KMapper is published on JitPack.
You can use this library on maven, gradle and any other build tools.
Please see here for the introduction method.
Example on maven
1. add repository reference for JitPack
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
2. add dependency
<dependency>
<groupId>com.github.ProjectMapK</groupId>
<artifactId>KMapper</artifactId>
<version>Tag</version>
</dependency>
Principle of operation
The behavior of KMapper is as follows.
- Get the
KFunctionto be called. - Analyze the
KFunctionand determine what arguments are needed and how to deserialize them. - Get the value for each argument from inputs and deserialize it. and call the
KFunction.
KMapper performs the mapping by calling a function, so the result is a Subject to the constraints on the argument and nullability.
That is, there is no runtime error due to breaking the null safety of Kotlin(The null safety on type arguments may be broken due to problems on the Kotlin side).
Also, it supports the default arguments which are peculiar to Kotlin.
Types of mapper classes
The project offers three types of mapper classes.
KMapperPlainKMapperBoundKMapper
Here is a summary of the features and advantages of each.
Also, the common features are explained using KMapper as an example.
KMapper
The KMapper is a basic mapper class for this project.
It is suitable for using the same instance of the class, since it is cached internally to speed up the mapping process.
PlainKMapper
PlainKMapper is a mapper class from KMapper without caching.
Although the performance is not as good as KMapper in case of multiple mappings, it is suitable for use as a disposable mapper because there is no overhead of cache processing.
BoundKMapper
BoundKMapper is a mapping class for the case where only one source class is available.
It is faster than KMapper.
Initialization
KMapper can be initialized from method reference(KFunction) to be called or the KClass to be mapped.
The following is a summary of each initialization.
However, some of the initialization of BoundKMapper are shown as examples simplified by a dummy constructor.
Initialize from method reference(KFunction)
When the primary constructor is the target of a call, you can initialize it as follows.
data class Dst(
foo: String,
bar: String,
baz: Int?,
...
)
// Get constructor reference
val dstConstructor: KFunction<Dst> = ::Dst
// KMapper
val kMapper: KMapper<Dst> = KMapper(dstConstructor)
// PlainKMapper
val plainMapper: PlainKMapper<Dst> = PlainKMapper(dstConstructor)
// BoundKMapper
val boundKMapper: BoundKMapper<Src, Dst> = BoundKMapper(dstConstructor)
Initialize from KClass
The KMapper can also be initialized from the KClass.
By default, the primary constructor is the target of the call.
data class Dst(...)
// KMapper
val kMapper: KMapper<Dst> = KMapper(Dst::class)
// PlainKMapper
val plainMapper: PlainKMapper<Dst> = PlainKMapper(Dst::class)
// BoundKMapper
val boundKMapper: BoundKMapper<Src, Dst> = BoundKMapper(Dst::class, Src::class)
By using a dummy constructor and omitting generics, you can also write as follows.
// KMapper
val kMapper: KMapper<Dst> = KMapper()
// PlainKMapper
val plainMapper: PlainKMapper<Dst> = PlainKMapper()
// BoundKMapper
val boundKMapper: BoundKMapper<Src, Dst> = BoundKMapper()
Specifying the target of a call by KConstructor annotation
When initializing from the KClass, all mapper classes can specify the function to be called by the KConstructor annotation.
In the following example, the secondary constructor is called.
data class Dst(...) {
@KConstructor
constructor(...) : this(...)
}
val mapper: KMapper<Dst> = KMapper(Dst::class)
Similarly, the following example calls the factory method.
data class Dst(...) {
companion object {
@KConstructor
fun factory(...): Dst {
...
}
}
}
val mapper: KMapper<Dst> = KMapper(Dst::class)
Detailed usage
Converting values during mapping
In mapping, you may want to convert one input type to another.
The KMapper provides a rich set of conversion features for such a situation.
However, this conversion can be performed under the following conditions.
- Input is not
null.- If
nullis involved, it is recommended to combine theKParameterRequireNonNullannotation with the default argument.
- If
- Input cannot be assigned directly to an argument.
Conversions available by default
Some of the conversion features are available without any special description.
1-to-1 conversion (nested mapping)
If you can't use arguments as they are and no other transformation is possible, KMapper tries to do 1-to-1 mapping using the mapping class.
This allows you to perform the following nested mappings by default.
data class InnerDst(val foo: Int, val bar: Int)
data class Dst(val param: InnerDst)
data class InnerSrc(val foo: Int, val bar: Int)
data class Src(val param: InnerSrc)
val src = Src(InnerSrc(1, 2))
val dst = KMapper(::Dst).map(src)
println(dst.param) // -> InnerDst(foo=1, bar=2)
Specifies the function used for the nested mapping
Nested mapping is performed by initializing BoundKMapper from the class.
For this reason, you can specify the target of the call with the KConstructor annotation.
Other conversions
Conversion from String to Enum
If the input is a String and the argument is an Enum, an attempt is made to convert the input to an Enum with the corresponding name.
enum class FizzBuzz {
Fizz, Buzz, FizzBuzz;
}
data class Dst(val fizzBuzz: FizzBuzz)
val dst = KMapper(::Dst).map("fizzBuzz" to "Fizz")
println(dst) // -> Dst(fizzBuzz=Fizz)
Conversion to String
If the argument is a String, the input is converted by toString method.
Specifying the conversion method using the KConverter annotation
If you create your own class and can be initialized from a single argument, you can use the KConverter annotation.
The KConverter annotation can be added to a constructor or a factory method defined in a companion object.
// Annotate the primary constructor
data class FooId @KConverter constructor(val id: Int)
// Annotate the secondary constructor
data class FooId(val id: Int) {
@KConverter
constructor(id: String) : this(id.toInt())
}
// Annotate the factory method
data class FooId(val id: Int) {
companion object {
@KConverter
fun of(id: String): FooId = FooId(id.toInt())
}
}
// If the fooId is given a KConverter, Dst can do the mapping successfully without doing anything.
data class Dst(
fooId: FooId,
bar: String,
baz: Int?,
...
)
Conversion by creating your own custom deserialization annotations
If you cannot use KConverter, you can convert it by creating a custom conversion annotations and adding it to the parameter.
Custom conversion annotation is made by defining a pair of conversion annotation and converter.
As an example, we will show how to create a ZonedDateTimeConverter that converts from java.sql.Timestamp or java.time.Instant to ZonedDateTime in the specified time zone.
Create conversion annotation
You can define a conversion annotation by adding @Target(AnnotationTarget.VALUE_PARAMETER), KConvertBy annotation, and several other annotations.
The argument of the KConvertBy annotation passes the KClass of the converter described below.
This converter should be defined for each source type.
Also, although this example defines an argument to the annotation, you can get the value of the annotation from the converter.
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@KConvertBy([TimestampToZonedDateTimeConverter::class, InstantToZonedDateTimeConverter::class])
annotation class ZonedDateTimeConverter(val zoneIdOf: String)
Create converter
You can define converter by inheriting AbstractKConverter<A, S, D>.
Generics A,S,D have the following meanings.
A:conversion annotationType.S: SourceType.D: DestinationType.
Below is an example of a converter that converts from java.sql.Timestamp to ZonedDateTime.
class TimestampToZonedDateTimeConverter(
annotation: ZonedDateTimeConverter
) : AbstractKConverter<ZonedDateTimeConverter, Timestamp, ZonedDateTime>(annotation) {
private val timeZone = ZoneId.of(annotation.zoneIdOf)
override val srcClass: KClass<Timestamp> = Timestamp::class
override fun convert(source: Timestamp): ZonedDateTime = ZonedDateTime.of(source.toLocalDateTime(), timeZone)
}
The argument to the converter's primary constructor should only take a conversion annotation.
This is called when KMapper is initialized.
As shown in the example, you can refer to the arguments defined in the annotation.
Using custom conversion annotations
The conversion annotation and the converter defined so far are written together as follows.
InstantToZonedDateTimeConverter is a converter whose source is java.time.Instant.
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@KConvertBy([TimestampToZonedDateTimeConverter::class, InstantToZonedDateTimeConverter::class])
annotation class ZonedDateTimeConverter(val zoneIdOf: String)
class TimestampToZonedDateTimeConverter(
annotation: ZonedDateTimeConverter
) : AbstractKConverter<ZonedDateTimeConverter, Timestamp, ZonedDateTime>(annotation) {
private val timeZone = ZoneId.of(annotation.zoneIdOf)
override val srcClass: KClass<Timestamp> = Timestamp::class
override fun convert(source: Timestamp): ZonedDateTime = ZonedDateTime.of(source.toLocalDateTime(), timeZone)
}
class InstantToZonedDateTimeConverter(
annotation: ZonedDateTimeConverter
) : AbstractKConverter<ZonedDateTimeConverter, Instant, ZonedDateTime>(annotation) {
private val timeZone = ZoneId.of(annotation.zoneIdOf)
override val srcClass: KClass<Instant> = Instant::class
override fun convert(source: Instant): ZonedDateTime = ZonedDateTime.ofInstant(source, timeZone)
}
When this is given, it becomes as follows.
data class Dst(
@ZonedDateTimeConverter("Asia/Tokyo")
val t1: ZonedDateTime,
@ZonedDateTimeConverter("-03:00")
val t2: ZonedDateTime
)
Conversion from Multiple Arguments
The KParameterFlatten annotation allows you to perform a transformation that requires more than one argument.
data class InnerDst(val fooFoo: Int, val barBar: String)
data class Dst(val bazBaz: InnerDst, val quxQux: LocalDateTime)
To specify a field name as a prefix, give it as follows.
The class specified with KParameterFlatten is initialized from the function or the primary constructor specified with the aforementioned KConstructor annotation.
data class InnerDst(val fooFoo: Int, val barBar: String)
data class Dst(
@KParameterFlatten
val bazBaz: InnerDst,
val quxQux: LocalDateTime
)
data class Src(val bazBazFooBoo: Int, val bazBazBarBar: String, val quxQux: LocalDateTime)
// required 3 arguments that bazBazFooFoo, bazBazBarBar, quxQux
val mapper = KMapper(::Dst)
KParameterFlatten annotation options
The KParameterFlatten annotation has two options for handling argument names of the nested classes.
fieldNameToPrefix
By default, the KParameterFlatten annotation tries to find a match by prefixing the name of the argument with the name of the prefix.
If you don't want to prefix the argument names, you can set the fieldNameToPrefix option to false.
data class InnerDst(val fooFoo: Int, val barBar: String)
data class Dst(
@KParameterFlatten(fieldNameToPrefix = false)
val bazBaz: InnerDst,
val quxQux: LocalDateTime
)
// required 3 arguments that fooFoo, barBar, quxQux
val mapper = KMapper(::Dst)
If fieldNameToPrefix = false is specified, the nameJoiner option is ignored.
nameJoiner
The nameJoiner specifies how to join argument names and argument names.
For example, if Src is snake_case, the following command is used.
data class InnerDst(val fooFoo: Int, val barBar: String)
data class Dst(
@KParameterFlatten(nameJoiner = NameJoiner.Snake::class)
val bazBaz: InnerDst,
val quxQux: LocalDateTime
)
// required 3 arguments that baz_baz_foo_foo, baz_baz_bar_bar, qux_qux
val mapper = KMapper(::Dst) { /* some naming transformation process */ }
By default, camelCase is specified, and snake_case and kebab-case are also supported.
You can also write your own by creating object which extends the NameJoiner class.
Use with other conversion methods
The KParameterFlatten annotation also works with all the conversion methods introduced so far.
Also, the KParameterFlatten annotation can be used in any number of layers of nested objects.
Set the argument names and field names used for mapping
By default, KMapper searches the source for a field whose name corresponds to the argument name.
On the other hand, there are times when you want to use a different name for the argument name and the source.
In order to deal with such a situation, KMapper provides some functions to set the argument name and field name used during mapping.
Conversion of argument names
With KMapper, you can set the argument name conversion function at initialization.
It can handle situations where constant conversion is required, for example, the argument naming convention is camel case and the source naming convention is snake case.
data class Dst(
fooFoo: String,
barBar: String,
bazBaz: Int?
)
val mapper: KMapper<Dst> = KMapper(::Dst) { fieldName: String ->
/* some naming transformation process */
}
// For example, by passing a conversion function to the snake case, the following input can be handled
val dst = mapper.map(mapOf(
"foo_foo" to "foo",
"bar_bar" to "bar",
"baz_baz" to 3
))
And, of course, any conversion process can be performed within the lambda.
Propagation of the argument name conversion process
The argument name conversion process is also reflected in the nested mapping.
Also, the conversion is applied to the aliases specified with the KParameterAlias annotation described below.
The actual conversion process
Although KMapper does not provide naming transformation, some of the most popular libraries in your project may also provide it.
Here is a sample code of Jackson and Guava that actually passes the "CamelCase -> SnakeCase" transformations.
Jackson
import com.fasterxml.jackson.databind.PropertyNamingStrategy
val parameterNameConverter: (String) -> String = PropertyNamingStrategy.SnakeCaseStrategy()::translate
val mapper: KMapper<Dst> = KMapper(::Dst, parameterNameConverter)
Guava
import com.google.common.base.CaseFormat
val parameterNameConverter: (String) -> String = { fieldName: String ->
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName)
}
val mapper: KMapper<Dst> = KMapper(::Dst, parameterNameConverter)
Set an alias for the getter
It is best to use the KGetterAlias annotation to rename the _foo field of the Scr class only at mapping time in the following code.
data class Dst(val foo: Int)
data class Src(val _foo: Int)
The actual grant is as follows.
data class Src(
@get:KGetterAlias("foo")
val _foo: Int
)
Set an alias to an argument name
It is best to use the KParameterAlias annotation if you want to change the name of the _bar field of the Dst class only at mapping time in the following code.
data class Dst(val _bar: Int)
data class Src(val bar: Int)
The actual grant is as follows.
data class Dst(
@KParameterAlias("bar")
val _bar: Int
)
Other functions
Control and use default arguments
The KMapper uses the default argument if no argument is given.
Also, if an argument is given, you can control whether to use it or not.
Always use the default arguments
If you want to force a default argument, you can use the KUseDefaultArgument annotation.
class Foo(
...,
@KUseDefaultArgument
val description: String = ""
)
Use default argument if input is null
The KParameterRequireNonNull annotation skips the input until a non null value is specified as an argument.
By using this, the default argument is used when all the corresponding contents are null.
class Foo(
...,
@KParameterRequireNonNull
val description: String = ""
)
Ignore the field when mapping
If you want to ignore a field for mapping for some reason, you can use the KGetterIgnore annotation.
For example, if you enter the following class of Src, the param1 field will not be read.
data class Src(
@KGetterIgnore
val param1: Int,
val param2: Int
)
Setting Up Arguments
Target for argument reading
The KMapper can read the public field of an object, or the properties of Pair<String, Any?> and Map<String, Any?>.
Setting Up Arguments
The KMapper performs the setup process if the value is not null.
In the setup process, first of all, parameterClazz.isSuperclassOf(inputClazz) is used to check if the input can be set as an argument or not, and if not, the conversion described later is performed and the result is used as an argument.
If the value is null, the KParameterRequireNonNull annotation is checked, and if it is set, the setup process is skipped, otherwise null is used as the argument.
If the KUseDefaultArgument annotation is set or all inputs are skipped by the KParameterRequireNonNull annotation, the default argument is used.
If the default argument is not available at this time, a runtime error occurs.
Conversion of arguments
KMapper performs conversion and checking in the following order.
1. Checking the specification of the conversion process by annotation
First of all, it checks for conversions specified by the KConvertBy and KConverter annotations for the class of the input.
2. Confirmation of conversion to Enum
If the input is a String and the argument is an Enum, the function tries to convert the input to an Enum with the corresponding name.
3. Confirmation of conversion to string
If the argument is String, the input will be toString.
4. Conversion using the mapper class
If the transformation does not meet the criteria so far, a mapping process is performed using a mapper class.
For this mapping process, PlainKMapper is used for PlainKMapper, and BoundKMapper is used for others.
Input priority
The KMapper basically gives priority to the first available argument.
For example, in the following example, since param1 is given first as value1, the next input param1" to "value2" is ignored.
val mapper: KMapper<Dst> = ...
val dst = mapper.map("param1" to "value1", "param1" to "value2")
However, if null is specified as an input for an argument with a KParameterRequireNonNull annotation, it is ignored and the later argument takes precedence.