IllegalArgumentException when trying to access nullable value class property value via reflection

(Crosspost from YouTrack)

I’ve noticed this weird issue when trying to access property values via reflection. When the type of the property is nullable and any of the unsigned primitives ( UByte? , UShort? , UInt? , ULong? ), I get an IllegalArgumentException on access via KProperty<T, *>.get() .

After some further digging, I found that this is an issue with all value classes.

Here is an MCVE:

import kotlin.reflect.KClass
import kotlin.reflect.full.declaredMemberProperties

@Suppress("UNCHECKED_CAST")
fun <T : Any> getValueViaCapturedType(propertyName: String, t: T) =
    (t::class as KClass<T>)
        .declaredMemberProperties
        .single { it.name == propertyName }
        .get(t)

object DoubleObject {
    val nonNullable: Double = 0.0
    val nullable: Double? = 0.0
}

object FloatObject {
    val nonNullable: Float = 0f
    val nullable: Float? = 0f
}

object LongObject {
    val nonNullable: Long = 0
    val nullable: Long? = 0
}

object IntObject {
    val nonNullable: Int = 0
    val nullable: Int? = 0
}

object ShortObject {
    val nonNullable: Short = 0
    val nullable: Short? = 0
}

object CharObject {
    val nonNullable: Char = Char(0)
    val nullable: Char? = Char(0)
}

object ByteObject {
    val nonNullable: Byte = 0
    val nullable: Byte? = 0
}

object ULongObject {
    val nonNullable: ULong = 0u
    val nullable: ULong? = 0u
}

object UIntObject {
    val nonNullable: UInt = 0u
    val nullable: UInt? = 0u
}

object UShortObject {
    val nonNullable: UShort = 0u
    val nullable: UShort? = 0u
}

object UByteObject {
    val nonNullable: UByte = 0u
    val nullable: UByte? = 0u
}

object StringObject {
    val nonNullable: String = ""
    val nullable: String? = ""
}

object UnitObject {
    val nonNullable: Unit = Unit
    val nullable: Unit? = Unit
}

object ValueObject {
    val nonNullable: SomeValueClass = SomeValueClass()
    val nullable: SomeValueClass? = SomeValueClass()
}

@JvmInline
value class MyValueClass(val value: Int = 0)

private val subjects = listOf(
    DoubleObject,
    FloatObject,
    LongObject,
    IntObject,
    ShortObject,
    CharObject,
    ByteObject,
    ULongObject,
    UIntObject,
    UShortObject,
    UByteObject,
    StringObject,
    UnitObject,
    ValueObject,
)

fun runTest(propertyName: String) {
    var maybeThrowable: Throwable? = null

    subjects.forEach { subject ->
        val paddedClassName = subject::class.simpleName!!.padEnd(12)
        val result = runCatching { getValueViaCapturedType(propertyName, subject) }

        result
            .map { "✓" }
            .getOrElse { "ERROR" }
            .also { println("$paddedClassName $it") }

        maybeThrowable = result.exceptionOrNull() ?: maybeThrowable
    }

    maybeThrowable?.run {
        println()
        println("last stack trace:")
        printStackTrace()
    }
}

fun main() {
    runTest("nonNullable")
    println()
    println()
    runTest("nullable")
}

Running this MCVE will yield the following results:

~~ nonNullable ~~
DoubleObject ✓
FloatObject  ✓
LongObject   ✓
IntObject    ✓
ShortObject  ✓
CharObject   ✓
ByteObject   ✓
ULongObject  ✓
UIntObject   ✓
UShortObject ✓
UByteObject  ✓
StringObject ✓
UnitObject   ✓
ValueObject  ✓


~~ nullable ~~
DoubleObject ✓
FloatObject  ✓
LongObject   ✓
IntObject    ✓
ShortObject  ✓
CharObject   ✓
ByteObject   ✓
ULongObject  ERROR
UIntObject   ERROR
UShortObject ERROR
UByteObject  ERROR
StringObject ✓
UnitObject   ✓
ValueObject  ERROR

last stack trace:
java.lang.IllegalArgumentException: argument type mismatch
   at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
   at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
   at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
   at java.base/java.lang.reflect.Method.invoke(Method.java:566)
   at kotlin.reflect.jvm.internal.calls.InlineClassAwareCaller.call(InlineClassAwareCaller.kt:145)
   at kotlin.reflect.jvm.internal.KCallableImpl.call(KCallableImpl.kt:108)
   at kotlin.reflect.jvm.internal.KProperty1Impl.get(KProperty1Impl.kt:35)
   at net.marvk.fs.bgl.ReflectionIssueTestKt.getValueViaCapturedType(ReflectionIssueTest.kt:11)
   at net.marvk.fs.bgl.ReflectionIssueTestKt.runTest(ReflectionIssueTest.kt:100)
   at net.marvk.fs.bgl.ReflectionIssueTestKt.main(ReflectionIssueTest.kt:120)
   at net.marvk.fs.bgl.ReflectionIssueTestKt.main(ReflectionIssueTest.kt)

Process finished with exit code 0

As one can see, an exception is being thrown as described.

I all out of ideas with this and it sure seems like a bug, but maybe I’m missing something? Is this a known limitation?

1 Like