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).
We are using the UserView default constructor as the method call receiver by using the Kotlin with() function. Inside the lambda function provided to with(), we use reflection to obtain a Map of member properties (with the member name as the key and the member property as the value) using User::class. memberProperties.
Add and update entries To add multiple entries at a time, use putAll() . Its argument can be a Map or a group of Pair s: Iterable , Sequence , or Array . Both put() and putAll() overwrite the values if the given keys already exist in the map. Thus, you can use them to update values of map entries.
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)
Storing Properties in a Map
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")}" })
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With