Reputation: 1530
I want to convert/map some "data" class objects to similar "data" class objects. For example, classes for web form to classes for database records.
data class PersonForm(
val firstName: String,
val lastName: String,
val age: Int,
// maybe many fields exist here like address, card number, etc.
val tel: String
)
// maps to ...
data class PersonRecord(
val name: String, // "${firstName} ${lastName}"
val age: Int, // copy of age
// maybe many fields exist here like address, card number, etc.
val tel: String // copy of tel
)
I use ModelMapper for such works in Java, but it can't be used because data classes are final (ModelMapper creates CGLib proxies to read mapping definitions). We can use ModelMapper when we make these classes/fields open, but we must implement features of "data" class manually. (cf. ModelMapper examples: https://github.com/jhalterman/modelmapper/blob/master/examples/src/main/java/org/modelmapper/gettingstarted/GettingStartedExample.java)
How to map such "data" objects in Kotlin?
Update: ModelMapper automatically maps fields that have same name (like tel -> tel) without mapping declarations. I want to do it with data class of Kotlin.
Update: The purpose of each classes depends on what kind of application, but these are probably placed in the different layer of an application.
For example:
These classes are similar, but are not the same.
I want to avoid normal function calls for these reasons:
Of course, a library that has similar feature is intended, but information of the Kotlin feature is also welcome (like spreading in ECMAScript).
Upvotes: 112
Views: 144368
Reputation: 81
LaMapper - is mapper for Kotlin, supports both - constructor parameters and properties mapping.
It uses bytecode generation at runtime, so performance is the same as handwritten code. No compiler plugins are required.
Example
fun PersonForm.toPersonRecord(): PersonRecord = LaMapper.copyFrom(this) {
PersonRecord::code from PersonForm::personCode // property mapping
PersonRecord::name from { "${it.firstName} ${it.lastName}" } // lambda mapping
// all other properties are mapped by name
}
val rec = person.toPersonRecord()
In addition it has various data-type conversions by default (numbers, dates, enums etc.).
And, as it uses property references instead of strings for fields, it is more safe for refactoring.
Disclaimer: I'm the author.
Upvotes: 3
Reputation: 1
This is the best way ever to use ModelMapper
in Kotlin
, no need for Converters
or TypeMappers
. just use .apply{}
extension function on the converted object:
modelmapper.map(myDTO, myEntity::clas.java).apply{
myDTO.bar = foo(myEntity.bar)
}
below applys to the problem:
data class PersonRecord(
var name: String? = null,
var age: Int? = null,
var tel: String? = null,
) {
companion object {
fun fromForm(personForm: PersonForm): PersonRecord = ModelMapper().map(personForm, PersonRecord::class.java)
.apply {
name = "${personForm.firstName} ${personForm.lastName}"
}
}
}
Upvotes: 0
Reputation: 1472
You can use kotlin extension
function for transfer/convert one object to another object
Example:
data class PersonForm(
val firstName: String? = null,
val lastName: String? = null,
val age: Int? = null,
// maybe many fields exist here like address, card number, etc.
val tel: String? = null
)
// maps to ...
data class PersonRecord(
val name: String? = null,
val age: Int? = null,
val tel: String
)
Now create kotlin extension function for Convert / transfer into PersonRecord
fun PersonForm.transform(): PersonRecord {
val name: String = this.firstName + " " + this.lastName
val age: Int? = this.age
val tel: String = this.tel.toString()
return PersonRecord(name, age, tel)
}
and now you will be able to use it
fun getInfo() {
val personrecord = PersonForm().transform()
val finalName = personrecord.name // This will be first name and last name
}
Upvotes: 1
Reputation: 3609
Try this kotlin library, it support both kotlin and java
https://github.com/krud-dev/shapeshift
Upvotes: 1
Reputation: 520
kMapper-object to object mapper specifically created for Kotlin. Uses compile time code generation, so no reflection. Interface description for a mapper looks like that:
@Mapper
internal interface BindMapper {
fun map(dto: BindDto, @Bind second: Int, @Bind third: SomeInternalDto, @Bind(to = "fourth") pr: Double): BindDomain
}
More examples here.
Disclaimer: I'm the author.
Upvotes: 1
Reputation: 111
You can use the DataClassMapper
class taken from here: https://github.com/jangalinski/kotlin-dataclass-mapper
data class PersonForm(
val firstName: String,
val lastName: String,
val age: Int,
// maybe many fields exist here like address, card number, etc.
val tel: String
)
// maps to ...
data class PersonRecord(
val name: String, // "${firstName} ${lastName}"
val age: Int, // copy of age
// maybe many fields exist here like address, card number, etc.
val tel: String // copy of tel
)
fun mapPerson(person: PersonForm): PersonRecord =
DataClassMapper<PersonForm, PersonRecord>()
.targetParameterSupplier(PersonRecord::name) { "${it.firstName} ${it.lastName}"}
.invoke(person)
fun main() {
val result = mapPerson(PersonForm("first", "last", 25, "tel"))
println(result)
}
Result will be:
PersonRecord(name=first last, age=25, tel=tel)
Upvotes: 0
Reputation: 291
MapStruct lets kapt generate classes doing the mapping (without reflection).
Use MapStruct:
@Mapper
interface PersonConverter {
@Mapping(source = "phoneNumber", target = "phone")
fun convertToDto(person: Person) : PersonDto
@InheritInverseConfiguration
fun convertToModel(personDto: PersonDto) : Person
}
// Note this either needs empty constructor or we need @KotlinBuilder as dsecribe below
data class Person: this(null, null, null, null) (...)
Use:
val converter = Mappers.getMapper(PersonConverter::class.java) // or PersonConverterImpl()
val person = Person("Samuel", "Jackson", "0123 334466", LocalDate.of(1948, 12, 21))
val personDto = converter.convertToDto(person)
println(personDto)
val personModel = converter.convertToModel(personDto)
println(personModel)
Edit:
Now with @KotlinBuilder for avoiding constructor() issue:
GitHub: Pozo's mapstruct-kotlin
Annotate data classes with @KotlinBuilder
. This will create a PersonBuilder
class, which MapStruct uses, thus we avoid ruining the interface of the data class with a constructor().
@KotlinBuilder
data class Person(
val firstName: String,
val lastName: String,
val age: Int,
val tel: String
)
Dependency :
// https://mvnrepository.com/artifact/com.github.pozo/mapstruct-kotlin
api("com.github.pozo:mapstruct-kotlin:1.3.1.1")
kapt("com.github.pozo:mapstruct-kotlin-processor:1.3.1.1")
https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-kotlin
Upvotes: 23
Reputation: 289
Using ModelMapper
/** Util.kt **/
class MapperDto() : ModelMapper() {
init {
configuration.matchingStrategy = MatchingStrategies.LOOSE
configuration.fieldAccessLevel = Configuration.AccessLevel.PRIVATE
configuration.isFieldMatchingEnabled = true
configuration.isSkipNullEnabled = true
}
}
object Mapper {
val mapper = MapperDto()
inline fun <S, reified T> convert(source: S): T = mapper.map(source, T::class.java)
}
Usage
val form = PersonForm(/** ... **/)
val record: PersonRecord = Mapper.convert(form)
You might need some mapping rules if the field names differ. See the getting started
PS: Use kotlin no-args
plugin for having default no-arg constructor with your data classes
Upvotes: 8
Reputation: 885
For ModelMapper you could use Kotlin's no-arg compiler plugin, with which you can create an annotation that marks your data class to get a synthetic no-arg constructor for libraries that use reflection. Your data class needs to use var
instead of val
.
package com.example
annotation class NoArg
@NoArg
data class MyData(var myDatum: String)
mm.map(. . ., MyData::class.java)
and in build.gradle (see docs for Maven):
buildscript {
. . .
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
apply plugin: 'kotlin-noarg'
noArg {
annotation "com.example.NoArg"
}
Upvotes: 3
Reputation: 791
You can use ModelMapper to map to a Kotlin data class. The keys are:
Mutable member, var instead of val
data class AppSyncEvent @JvmOverloads constructor( var field: String = "", var arguments: Map<String, *> = mapOf<String, Any>(), var source: Map<String, *> = mapOf<String, Any>() ) val event = ModelMapper().map(request, AppSyncEvent::class.java)
Upvotes: 2
Reputation: 1382
This works using Gson:
inline fun <reified T : Any> Any.mapTo(): T =
GsonBuilder().create().run {
toJson(this@mapTo).let { fromJson(it, T::class.java) }
}
fun PersonForm.toRecord(): PersonRecord =
mapTo<PersonRecord>().copy(
name = "$firstName $lastName"
)
fun PersonRecord.toForm(): PersonForm =
mapTo<PersonForm>().copy(
firstName = name.split(" ").first(),
lastName = name.split(" ").last()
)
with not nullable values allowed to be null because Gson uses sun.misc.Unsafe..
Upvotes: 4
Reputation: 25011
Is this are you looking for?
data class PersonRecord(val name: String, val age: Int, val tel: String){
object ModelMapper {
fun from(form: PersonForm) =
PersonRecord(form.firstName + form.lastName, form.age, form.tel)
}
}
and then:
val personRecord = PersonRecord.ModelMapper.from(personForm)
Upvotes: 27
Reputation: 31274
Simplest (best?):
fun PersonForm.toPersonRecord() = PersonRecord(
name = "$firstName $lastName",
age = age,
tel = tel
)
Reflection (not great performance):
fun PersonForm.toPersonRecord() = with(PersonRecord::class.primaryConstructor!!) {
val propertiesByName = PersonForm::class.memberProperties.associateBy { it.name }
callBy(args = parameters.associate { parameter ->
parameter to when (parameter.name) {
"name" -> "$firstName $lastName"
else -> propertiesByName[parameter.name]?.get(this@toPersonRecord)
}
})
}
Cached reflection (okay performance but not as fast as #1):
open class Transformer<in T : Any, out R : Any>
protected constructor(inClass: KClass<T>, outClass: KClass<R>) {
private val outConstructor = outClass.primaryConstructor!!
private val inPropertiesByName by lazy {
inClass.memberProperties.associateBy { it.name }
}
fun transform(data: T): R = with(outConstructor) {
callBy(parameters.associate { parameter ->
parameter to argFor(parameter, data)
})
}
open fun argFor(parameter: KParameter, data: T): Any? {
return inPropertiesByName[parameter.name]?.get(data)
}
}
val personFormToPersonRecordTransformer = object
: Transformer<PersonForm, PersonRecord>(PersonForm::class, PersonRecord::class) {
override fun argFor(parameter: KParameter, data: PersonForm): Any? {
return when (parameter.name) {
"name" -> with(data) { "$firstName $lastName" }
else -> super.argFor(parameter, data)
}
}
}
fun PersonForm.toPersonRecord() = personFormToPersonRecordTransformer.transform(this)
data class PersonForm(val map: Map<String, Any?>) {
val firstName: String by map
val lastName: String by map
val age: Int by map
// maybe many fields exist here like address, card number, etc.
val tel: String by map
}
// maps to ...
data class PersonRecord(val map: Map<String, Any?>) {
val name: String by map // "${firstName} ${lastName}"
val age: Int by map // copy of age
// maybe many fields exist here like address, card number, etc.
val tel: String by map // copy of tel
}
fun PersonForm.toPersonRecord() = PersonRecord(HashMap(map).apply {
this["name"] = "${remove("firstName")} ${remove("lastName")}"
})
Upvotes: 96
Reputation: 33839
Do you really want a separate class for that? You can add properties to the original data class:
data class PersonForm(
val firstName: String,
val lastName: String,
val age: Int,
val tel: String
) {
val name = "${firstName} ${lastName}"
}
Upvotes: 4