kotlinkotlin-reflect

Deserialize map of field names and values to data class based on KClass (Reflection)


I'd like to construct an object from an instance of KClass and a Map<String, String> where I map fieldnames to values.

An example would be:

val testVal = mapOf("title" to "Foo", "description" to "bar")

data class Article(val title: String, val description)

fun <T> deserialize(clazz: KClass<T>, Map<String,String>) : T?  =  //magic

val article = deserialize(Article::class, testVal) 
// here article should be Article{title: "Foo", description: "Bar"} 

My question is how would I do this with Kotlin reflection? How do I build an object from the available data? I think it is enough if you point me to the right direction or give an idea. I mean there is definitely a way because I think this is what json serializers do (I just want a simpler version of that with much less functionality, on different dataformat).

I am happy if I can make this work with data classes that can only store primitive data types (int, string, boolean).

I know I could use something like Jackson and just build a JsonNode and deserialize it, but that is not waht I look for. I'd like to build my own version of this with reflection.


Solution

  • Actually, not all serialization libraries use reflection. For instance, magic in kotlinx.serialization is based on additional bytecode generated by compiler plugin. It is generally faster than runtime calls to Reflection API and I'm not sure that "simpler version" will be the same as "more performant" in this case.

    If you are still into inventing the wheel, this is how it could be done with reflection:

    fun <T : Any> deserialize(clazz: KClass<T>, properties: Map<String, *>): T {
        val primaryConstructor = clazz.primaryConstructor!! //all data classes has it
        val parameters = primaryConstructor.parameters
            .associateWith { properties[it.name] }
            //Parameters with nullable types or default values in constructor may be omited in JSON
            .filterNot { (parameter, value) -> value == null && parameter.isOptional }
            .toMap()
        return primaryConstructor.callBy(parameters)
    }
    

    You may simplify API a bit if you use reified type parameter:

    inline fun <reified T : Any> deserialize(properties: Map<String, *>): T {
        val clazz: KClass<T> = T::class
        //other code is the same
    }
    

    Note, that this code snippet doesn't provide any type convertion, and will throw a runtime exception if there is any non-String parameter in T (and Map<String, String> was passed to properties).