kotlinkotlin-reflect

How to set value of kotlin object-class property using kotlin.reflect API?


How can I set property value for classes with object keyword using kotlin.reflect API? I was hoping that this is analogous to setting values in regular classes, however, this is not the case. When setting a field value, I get an exception indicating the wrong number of parameters, which even looks to me like a bug in the language itself (I would expect that if, for some reason, setting a value for this type of class was not possible, I would rather receive an OperationNotSupported-style exception).

Here is an minimal example to reproduce the problem:

import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.jvm.isAccessible

fun main() {
    test(RegularClass::class, RegularClass())   //This one prints "Hello world" as expected!
    
    /* Both of these methods throw "java.lang.IllegalArgumentException: Callable expects 1 arguments, but 2 were provided"
     * How to set value of kotlin "object" property? */
    test(ObjectClass::class, null)
    test(ObjectClass::class, ObjectClass)
}

private fun test(kClass: KClass<out Greeter>, receiver: Greeter?) {
    val property = kClass.declaredMemberProperties
        .filterIsInstance<KMutableProperty1<Any?, Any?>>()
        .first {
            it.name == "variable"
        }

    property.isAccessible = true
    property.set(receiver, "Hello world!")
    receiver?.helloWorld()
}

interface Greeter {
    fun helloWorld()
}

class RegularClass: Greeter {
    private var variable: String = "Wrong value"

    override fun helloWorld() {
        println(variable)
    }
}

object ObjectClass: Greeter {
    private var variable: String = "Wrong value"

    override fun helloWorld() {
        println(variable)
    }
}

Solution

  • This is indeed a Kotlin bug. A private property in an object gets compiled to a Java static field, which does not need a receiver parameter to be set. There are three different issues on YouTrack regarding this issue, for getters: KT-55449, KT-55872, and KT-23267.

    A workaround for now is to detect such a field, and set it using Java reflection.

    private fun test(kClass: KClass<out Greeter>, receiver: Greeter?) {
        val property = kClass.declaredMemberProperties
            .filterIsInstance<KMutableProperty1<Any?, Any?>>()
            .first {
                it.name == "variable"
            }
    
        property.isAccessible = true
        val field = property.javaField
        if ((field?.modifiers?.and(Modifier.STATIC or Modifier.PRIVATE) ?: 0) > 0) {
            field?.set(null, "Hello World!")
        } else {
            property.set(receiver, "Hello world!")
        }
        receiver?.helloWorld()
    }