Better way to map Kotlin data objects to data objects
Asked Answered
Y

14

110

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:

  • data from database (Database Entity) to data for HTML form (Model/View Model)
  • REST API result to data for database

These classes are similar, but are not the same.

I want to avoid normal function calls for these reasons:

  • It depends on the order of arguments. A function for a class with many fields that have same type (like String) will be broken easily.
  • Many declarations are nesessary though most mappings can be resolved with naming convention.

Of course, a library that has similar feature is intended, but information of the Kotlin feature is also welcome (like spreading in ECMAScript).

Yahrzeit answered 29/8, 2016 at 6:1 Comment(4)
Please describe how you want to use the mapped classes. What is the purpose of having two separate data formats?Conchitaconchobar
Never heard of duplication the data model (except for cases of legacy code). Usually the data you are working with (View Model) is the data you put into the database.Conchitaconchobar
@Conchitaconchobar one use case would be to expose only parts of the domain model to different API consumers. Having separate DTO per view of domain model is much cleaner than using i.e. JsonView IMHOHeist
Do you really need these classes to be data classes?Donalddonaldson
P
96
  1. Simplest (best?):

    fun PersonForm.toPersonRecord() = PersonRecord(
            name = "$firstName $lastName",
            age = age,
            tel = tel
    )
    
  2. 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)
            }
        })
    }
    
  3. 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)
    
  4. 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")}"
    })
    
Pituitary answered 29/8, 2016 at 19:25 Comment(2)
MapStruct with @KotlinBuilder is a beautiful and fast solution. See other answer (where I've added @KotlinBuilder info).Spica
Actually I've stopped using MapStruct now, and just go with the first solution mentioned, ie. manual mapping. MapStruct is java-based, so does not give null-safety. Also I find I get compile-time safety anyway with simple kotlin initializer, when there are no default values on constructor. So if a field is added on the data object, I get a compile error, which is what I want.Spica
C
27

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)
Cochrane answered 29/8, 2016 at 8:31 Comment(1)
The operation that you write is I wanted to do. But I want to reduce mapping declarations because many fields that have same names (like tel -> tel) exist. I want to write only special rules like firstName + lastName => name.Yahrzeit
S
22

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

Spake answered 27/6, 2018 at 5:46 Comment(2)
You forgot to mention "tiny" detail. Your data classes have to be mutable and you have to have custom constructor of constructor() : this(null, null, null, null). So until mapstruct team comes up with proper kotlin support, I'd avoid using it and go with manual conversion as @Pituitary mentioned in his first solution.Condescend
I would also recommend MapStruct since it doesn't use reflection and is thus faster. For the constructor issue you can use this extension github.com/Pozo/mapstruct-kotlin which allows you to use the builder approach not requiring the constructor() : this(null, null) fiasco anymoreLarrup
C
8

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

Cringe answered 2/9, 2019 at 13:7 Comment(0)
C
4

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}"
}
Conchitaconchobar answered 29/8, 2016 at 13:30 Comment(2)
A spearate class is not required nesessarilly. But I want to avoid depending on the order of arguments.Yahrzeit
@Yahrzeit You can always use named arguments when constructing your data objects so that you don't depend on the order the arguments are defined in.Pituitary
L
4

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..

Leonorleonora answered 12/3, 2018 at 10:57 Comment(1)
Gson is quite sluggishAccolade
P
3

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"
}
Permutation answered 1/7, 2019 at 15:6 Comment(0)
M
2

You can use ModelMapper to map to a Kotlin data class. The keys are:

  • Use @JvmOverloads (generates a constructor with no arguments)
  • Default values for data class member
  • 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)
    
Mite answered 1/8, 2018 at 18:48 Comment(0)
K
1

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.

Kurdistan answered 13/11, 2022 at 20:32 Comment(0)
G
1

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.

Gauvin answered 20/12, 2022 at 14:41 Comment(1)
Looks promising, but please add a disclaimer, that you are the author of kmapperFamed
F
1

Try this kotlin library, it support both kotlin and java

https://github.com/krud-dev/shapeshift

Farsighted answered 17/5, 2023 at 2:47 Comment(0)
M
1

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
}
Microelement answered 9/6, 2023 at 8:47 Comment(0)
P
0

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)
Phosphoroscope answered 11/10, 2022 at 6:34 Comment(0)
Q
0

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}"
            }
    }
}
Quatrefoil answered 20/7, 2023 at 5:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.