How to convert a Kotlin data class object to map?

39,572

Solution 1

I was using the jackson method, but turns out the performance of this is terrible on Android for first serialization (github issue here). And its dramatically worse for older android versions, (see benchmarks here)

But you can do this much faster with Gson. Conversion in both directions shown here:

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

val gson = Gson()

//convert a data class to a map
fun <T> T.serializeToMap(): Map<String, Any> {
    return convert()
}

//convert a map to a data class
inline fun <reified T> Map<String, Any>.toDataClass(): T {
    return convert()
}

//convert an object of type I to type O
inline fun <I, reified O> I.convert(): O {
    val json = gson.toJson(this)
    return gson.fromJson(json, object : TypeToken<O>() {}.type)
}

//example usage
data class Person(val name: String, val age: Int)

fun main() {

    val person = Person("Tom Hanley", 99)

    val map = mapOf(
        "name" to "Tom Hanley", 
        "age" to 99
    )

    val personAsMap: Map<String, Any> = person.serializeToMap()

    val mapAsPerson: Person = map.toDataClass()
}

Solution 2

This extension function uses reflection, but maybe it'll help someone like me coming across this in the future:

inline fun <reified T : Any> T.asMap() : Map<String, Any?> {
    val props = T::class.memberProperties.associateBy { it.name }
    return props.keys.associateWith { props[it]?.get(this) }
}

Solution 3

I have the same use case today for testing and ended up i have used Jackson object mapper to convert Kotlin data class into Map. The runtime performance is not a big concern in my case. I haven't checked in details but I believe it's using reflection under the hood but it's out of concern as happened behind the scene.

For Example,

val dataclass = DataClass(p1 = 1, p2 = 2)
val dataclassAsMap = objectMapper.convertValue(dataclass, object: 
TypeReference<Map<String, Any>>() {})

//expect dataclassAsMap == mapOf("p1" to 1, "p2" to 2)

Solution 4

kotlinx.serialization has an experimental Properties format that makes it very simple to convert Kotlin classes into maps and vice versa:

@ExperimentalSerializationApi
@kotlinx.serialization.Serializable
data class Category constructor(
    val id: Int,
    val name: String,
    val icon: String,
    val numItems: Long
) {

    // the map representation of this class
    val asMap: Map<String, Any> by lazy { Properties.encodeToMap(this) }

    companion object {
        // factory to create Category from a map
        fun from(map: Map<String, Any>): Category =
            Properties.decodeFromMap(map)
    }
}

Solution 5

The closest you can get is with delegated properties stored in a map.

Example (from link):

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

Using this with data classes may not work very well, however.

Share:
39,572
Mac
Author by

Mac

Updated on July 12, 2022

Comments

  • Mac
    Mac almost 2 years

    Is there any easy way or any standard library method to convert a Kotlin data class object to a map/dictionary of its properties by property names? Can reflection be avoided?

  • EpicPandaForce
    EpicPandaForce about 6 years
    His problem is that he wants this the other way around: turn a data class into Map<String, Any?>.
  • apetranzilla
    apetranzilla about 6 years
    @EpicPandaForce I'm aware, but there's no non-reflective way to do that in Kotlin. The closest alternative would be to use delegated properties in a map. (i.e. you'd assign values to the map in the constructor, rather than properties)
  • EpicPandaForce
    EpicPandaForce about 6 years
    Yes of course, I'm pretty sure reflection is required.
  • Simon Forsberg
    Simon Forsberg over 4 years
    Nice solution, although just a note: This does not look like it will be working for nested data structures.
  • Ahsan Naseem
    Ahsan Naseem over 3 years
    using above i am getting No type arguments expected for class TypeReference using jackson 2.10.0
  • Serge Tahé
    Serge Tahé over 3 years
    How to use these functions ? Can you show some code ?
  • Tom Hanley
    Tom Hanley over 3 years
    sure, I've added some example usage to the answer
  • Serge Tahé
    Serge Tahé over 3 years
    I tried your code and I got two nulls for [personAsMap] and [mapAsPerson] as i got before my comment. That's why i asked some examples. Is this code working for you ? I am using Kotlin 1.4.0
  • Tom Hanley
    Tom Hanley over 3 years
    Its definitely working fine for me. How are you trying to run it? Do you have gson in your class path? I've added the gson imports if that helps. I'm using kotlin 1.4 also and gson 2.8.6, but i don't think this is a version issue.
  • Serge Tahé
    Serge Tahé over 3 years
    Hi Tom, i wanted to show the code executed. So i put it in an answer to the original question. Comments are to short for this.
  • Tom Hanley
    Tom Hanley over 3 years
    Ok I tried out your code and got the same. Debugging, gson doesn't seem to work properly on local classes. If you move the person class declaration outside of your function, so its a top level class, it works.
  • Jeremy Jao
    Jeremy Jao over 3 years
    I don't recommend this solution. GSON doesn't work properly in Kotlin since it's unaware of its null vs. non-null Kotlin compile-time rules. This is a better solution but doesn't handle nested properties, only primitives: stackoverflow.com/a/59316850/4425374
  • Nalawala Murtuza
    Nalawala Murtuza about 3 years
    in my case int convert to double example 1 -> 1.0
  • Nalawala Murtuza
    Nalawala Murtuza about 3 years
    Do you have a solution for nested data structure?
  • Nalawala Murtuza
    Nalawala Murtuza about 3 years
    nested class int property converts to float property.
  • Andrew
    Andrew about 3 years
    Unresolved reference "memberProperties". It is now called "members"
  • bugs_
    bugs_ almost 3 years
    No type arguments expected for class TypeReference be sure, that you are using class com.fasterxml.jackson.core.type.TypeReference if you import for example org.springframework.asm.TypeReference you will get this message :)
  • Rescribet
    Rescribet almost 3 years
    This also accounts for @SerialName annotations
  • erksch
    erksch over 2 years
    This seems to work only if the data class is flat and has no properties that are lists or maps.