I was trying to convert a Map<String, Any>
to data class instance:
data class Person(
val name: String,
val age: Int,
val weight: Double?,
)
fun test() {
val map = mapOf(
"name" to "steven",
"age" to 30,
"weight" to 60,
)
val ctor = Person::class.constructors.first();
val params = ctor.parameters.associateWith {
map[it.name]
}
val instance = ctor.callBy(params)
println(instance)
}
The code above throws java.lang.IllegalArgumentException: argument type mismatch
because 60
is passed to weight
as an Int, and kotlin does not support implicit conversion.
Then I change the type of weight
to non-nullable Double
data class Person(
val name: String,
val age: Int,
val weight: Double, // !
)
and that works, even 60F
works too.
My question is:
Why implicit conversion works only when type is non-nullable?
How to do implicit conversion when type is nullable?
Assuming this is Kotlin/JVM...
After digging a bit into how callBy
is implemented on the JVM, I found that it eventually calls Constructor.newInstance
. The documentation for that says:
Individual parameters are automatically unwrapped to match primitive formal parameters, and both primitive and reference parameters are subject to method invocation conversions as necessary.
When you use the non-nullable Double
in Kotlin as a parameter type, it is translated to the primitive type double
in the JVM world. However, if you use the nullable type Double?
, it is translated to the reference type wrapper java.lang.Double
. This is because the primitive type double
cannot be null. A similar thing happens for Int
and Int?
.
The "method invocation conversions" that the newInstance
docs mentioned (which I think is referring to the list of conversions allowed in an invocation context) does not include a conversion from int
to java.lang.Double
. After all, this Java code does not compile:
public static void foo(Double d) {}
// ...
int x = 1;
foo(x)l
But it would have compiled if foo
had taken the primitive double
.
As for your second question, I can't think of anything better than just checking each argument and parameter type manually, and performing the conversion explicitly for each case.
If you can change Person
, I'd suggest adding a secondary constructor that takes a non-nullable Double
, and change the calling code to choose an appropriate constructor.
Or if you really don't like choosing constructors, change the existing one to take Number?
, and convert it to an appropriate numeric type using the toXXX
methods before using it.