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.
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
)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.
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>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.
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.
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 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 is a mapping class for the case where only one source class is available.
It is faster than KMapper.
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.
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)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()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)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.
Some of the conversion features are available without any special description.
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)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.
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)If the argument is a String, the input is converted by toString method.
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?,
...
)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.
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)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.
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.
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
)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)The KParameterFlatten annotation has two options for handling argument names of the nested classes.
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.
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.
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.
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.
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.
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.
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.
importcom.fasterxml.jackson.databind.PropertyNamingStrategyval parameterNameConverter: (String) ->String=PropertyNamingStrategy.SnakeCaseStrategy()::translate
val mapper:KMapper<Dst> =KMapper(::Dst, parameterNameConverter)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)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
)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
)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.
If you want to force a default argument, you can use the KUseDefaultArgument annotation.
classFoo(
...,
@KUseDefaultArgument
valdescription:String = ""
)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 = ""
)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
)The KMapper can read the public field of an object, or the properties of Pair<String, Any?> and Map<String, Any?>.
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.
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.
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.