It all started with a comment
A few weeks back, I came across a very confusing comment in one of the projects I have been working on. Here’s the modified screenshot (and the code is here) (All the classes have their names changed to protect their privacy 😛)
The comment mentions that the casting is required for the code to work properly, however, IDEA is quite confident that we can remove it. Everything also works fine after we take the suggestion from IDEA to remove this casting.
What's going on here? Why did we need this casting (thus the comment)? Let’s (git) blame and see the diff!
Let's see the output from the code on the left and compare that to the output from the code on the right.
Things gets interesting
Those were interesting outputs, the polymorphic serialization works for engine but not for vehicle. Let's read the documentation for Json.encodeToString to dig a bit deeper.
So, the type of the passed value is being used to determine the serializer, that means when is doing some magic with the resulting types. Let's see what the documentation for when has to say about it.
Nothing! There's noting in the documentation about how when decides the returning type (at the time of writing this article). Let's go to the Kotlin language specification for it.
The type of the resulting when expression is the least upper bound of the types of all its entries. If when expression is not exhaustive, it has type kotlin.Unit and may be used only as a statement.
Now the left output makes sense, the engine's when expression is resolving its return type to Engine sealed class however the vehicle's when expression is resolving its return type to Bike class NOT to Vehicle sealed class. (We can also verify it by enabling the 'Local variable types' inlay hints, see screenshots below)
Based on these resultant return types, the Json.encodeToString function is resolving the relevant serializers, the Engine.serializer() and the Bike.serializer() respectively. Thereby giving us the output where the polymorphic Engine.serializer() adds types to the various engines but the Bike.serializer() doesn't.
Going back to the comment from where we started this investigation, the casting worked as the type was being derived from passed value type which was forced to be Vehicle sealed class. Also, the suggestion from IDEA makes sense as we introduced a new branch of CAR vehicle type, the resultant type for vehicle's when expression changed to Vehicle sealed class, thereby using the intended polymorphic Vehicle.serializer().
Summary
when expressions decide what to return by the least upper bound of types of all the entries. We should be mindful of this fact when introducing branches/entries in the existing when expression.
Instead of casting using as we could also have achieved the same results:
- By defining the type explicitly eg. val vehicle: Vehicle = when (vehicleType) { ... }
- By passing the intended serializer along with the value to be serialized eg. Json.encodeToString(Vehicle.serializer(), vehicle)
- By passing the type to derive the serializer eg. Json.encodeToString(vehicle)