Skip to content
This repository was archived by the owner on Jan 20, 2023. It is now read-only.

Repository files navigation

LicenseCircleCIcodecov


日本語版


KMapper

KMapper is a object to object mapper library for Kotlin, which provides the following features.

  • Bean mapping with Objects, Map, and Pair as 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 KMapperval 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.

  1. Get the KFunction to be called.
  2. Analyze the KFunction and determine what arguments are needed and how to deserialize them.
  3. 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.

  • KMapper
  • PlainKMapper
  • BoundKMapper

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 classDst(
foo:String,
bar:String,
baz:Int?,
...
)
// Get constructor referenceval dstConstructor:KFunction<Dst> = ::Dst// KMapperval kMapper:KMapper<Dst> =KMapper(dstConstructor)
// PlainKMapperval plainMapper:PlainKMapper<Dst> =PlainKMapper(dstConstructor)
// BoundKMapperval 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 classDst(...)
// KMapperval kMapper:KMapper<Dst> =KMapper(Dst::class)
// PlainKMapperval plainMapper:PlainKMapper<Dst> =PlainKMapper(Dst::class)
// BoundKMapperval boundKMapper:BoundKMapper<Src, Dst> =BoundKMapper(Dst::class, Src::class)

By using a dummy constructor and omitting generics, you can also write as follows.

// KMapperval kMapper:KMapper<Dst> =KMapper()
// PlainKMapperval plainMapper:PlainKMapper<Dst> =PlainKMapper()
// BoundKMapperval 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 classDst(...) {
@KConstructor
constructor(...) :this(...)
}
val mapper:KMapper<Dst> =KMapper(Dst::class)

Similarly, the following example calls the factory method.

data classDst(...) {
companionobject {
@KConstructor
funfactory(...): 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 null is involved, it is recommended to combine the KParameterRequireNonNull annotation with the default argument.
  • 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 classInnerDst(valfoo:Int, valbar:Int)
data classDst(valparam:InnerDst)
data classInnerSrc(valfoo:Int, valbar:Int)
data classSrc(valparam: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.

enumclassFizzBuzz {
Fizz, Buzz, FizzBuzz;
}
data classDst(valfizzBuzz: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 constructordata classFooId @KConverter constructor(valid:Int)
// Annotate the secondary constructordata classFooId(valid:Int) {
@KConverter
constructor(id:String) :this(id.toInt())
}
// Annotate the factory methoddata classFooId(valid:Int) {
companionobject {
@KConverter
funof(id:String): FooId=FooId(id.toInt())
}
}
// If the fooId is given a KConverter, Dst can do the mapping successfully without doing anything.data classDst(
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])
annotationclassZonedDateTimeConverter(valzoneIdOf: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: Source Type.
  • D: Destination Type.

Below is an example of a converter that converts from java.sql.Timestamp to ZonedDateTime.

classTimestampToZonedDateTimeConverter(
annotation:ZonedDateTimeConverter
) : AbstractKConverter<ZonedDateTimeConverter, Timestamp, ZonedDateTime>(annotation) {
privateval timeZone =ZoneId.of(annotation.zoneIdOf)
overrideval srcClass:KClass<Timestamp> =Timestamp::classoverridefunconvert(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])
annotationclassZonedDateTimeConverter(valzoneIdOf:String)
classTimestampToZonedDateTimeConverter(
annotation:ZonedDateTimeConverter
) : AbstractKConverter<ZonedDateTimeConverter, Timestamp, ZonedDateTime>(annotation) {
privateval timeZone =ZoneId.of(annotation.zoneIdOf)
overrideval srcClass:KClass<Timestamp> =Timestamp::classoverridefunconvert(source:Timestamp): ZonedDateTime=ZonedDateTime.of(source.toLocalDateTime(), timeZone)
}
classInstantToZonedDateTimeConverter(
annotation:ZonedDateTimeConverter
) : AbstractKConverter<ZonedDateTimeConverter, Instant, ZonedDateTime>(annotation) {
privateval timeZone =ZoneId.of(annotation.zoneIdOf)
overrideval srcClass:KClass<Instant> =Instant::classoverridefunconvert(source:Instant): ZonedDateTime=ZonedDateTime.ofInstant(source, timeZone)
}

When this is given, it becomes as follows.

data classDst(
@ZonedDateTimeConverter("Asia/Tokyo")
valt1:ZonedDateTime,
@ZonedDateTimeConverter("-03:00")
valt2:ZonedDateTime
)

Conversion from Multiple Arguments

The KParameterFlatten annotation allows you to perform a transformation that requires more than one argument.

data classInnerDst(valfooFoo:Int, valbarBar:String)
data classDst(valbazBaz:InnerDst, valquxQux: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 classInnerDst(valfooFoo:Int, valbarBar:String)
data classDst(
@KParameterFlatten
valbazBaz:InnerDst,
valquxQux:LocalDateTime
)
data classSrc(valbazBazFooBoo:Int, valbazBazBarBar:String, valquxQux:LocalDateTime)
// required 3 arguments that bazBazFooFoo, bazBazBarBar, quxQuxval 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 classInnerDst(valfooFoo:Int, valbarBar:String)
data classDst(
@KParameterFlatten(fieldNameToPrefix =false)
valbazBaz:InnerDst,
valquxQux:LocalDateTime
)
// required 3 arguments that fooFoo, barBar, quxQuxval 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 classInnerDst(valfooFoo:Int, valbarBar:String)
data classDst(
@KParameterFlatten(nameJoiner =NameJoiner.Snake::class)
valbazBaz:InnerDst,
valquxQux:LocalDateTime
)
// required 3 arguments that baz_baz_foo_foo, baz_baz_bar_bar, qux_quxval 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 classDst(
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 handledval 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
importcom.fasterxml.jackson.databind.PropertyNamingStrategyval parameterNameConverter: (String) ->String=PropertyNamingStrategy.SnakeCaseStrategy()::translate
val mapper:KMapper<Dst> =KMapper(::Dst, parameterNameConverter)
Guava
importcom.google.common.base.CaseFormatval 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 classDst(valfoo:Int)
data classSrc(val_foo:Int)

The actual grant is as follows.

data classSrc(
@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 classDst(val_bar:Int)
data classSrc(valbar:Int)

The actual grant is as follows.

data classDst(
@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.

classFoo(
...,
@KUseDefaultArgument
valdescription: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.

classFoo(
...,
@KParameterRequireNonNull
valdescription: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 classSrc(
@KGetterIgnore
valparam1:Int,
valparam2: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.

Releases

Packages

Used by

Contributors

Languages