Ad-hoc polymorphism in JSON with Kotlin
For a long time now JSON is a de facto standard for all kinds of data serialization between client and server. Among other, its strengths are simplicity and human-readability. But with simplicity comes some limitations, one of them I would like to talk about today: storing and retrieving polymorphic objects.
The need to parse JSON and also convert objects to JSON is pretty much universal, so in all likeliness, you are already using a JSON library in your code.
First of, there is the Awesome-Kotlin list about JSON libraries. Then, there are multiple articles like this one, talking about how to handle Kotlin data classes with JSON.
We want to use Kotlin data classes for concise code, non-nullable types for null-safety and default arguments for the data class constructor to work when a field is missing in a given JSON. We also would probably want explicit exceptions when the mapping fails completely (required field missing). We also want near zero overhead automatic mapping from JSON to objects and in reverse. On android, we also want a small APK size, so a reduced number of dependencies and small libraries. Therefore:
- We don’t want to use android’s
org.json
, because it has very limited capabilities and no mapping functionality at all. - To my knowledge, to make use of the described Kotlin features like null-safety and default arguments, all libraries supporting Kotlin fully use
kotlin-reflect
, which is around 2MB in size and therefore might not be an option. - We might not have the ability to use a library like Moshi with integrated Kotlin support, because we already use the popular Gson or Jackson library used in the project.
This post describes a way of using the GSON and Mosi library with Kotlin data classes and the least amount of overhead possible of achieving a mapping of JSON to Kotlin data classes with null-safety and default values with Polymorphic JSON data.
First we need to understand the polymorphism on the data we are trying to parse:
Polymorphism by field value, aka discriminator - help's detect the object type, an API can add the discriminator/propertyName
keyword to model definitions. This keyword points to the property that specifies the data type.
Discriminator embedded in object -polymorphic classes
[
{
"type":"CIRCLE",
"radius":10.0
},
{
"type":"RECTANGLE",
"width":20.0
}
]
In this case , because only Circle has radius field, first object from list will be deserialized in Circle class.
One solution for this could be :
sealed class Shape
class Circle(val radius: Double) : Shape
class Rectangle(val width: Double) : Shape
Discriminator is external - polymorphic fields
[
{
"type":"CIRCLE",
"data":{
"radius":10.0
}
},
{
"type":"RECTANGLE",
"data":{
"width":20.0
}
}
]
In this case , since the discriminator is external , we need a mechanism to decide the data type and deserialize our JSON to our respective data classes.
GSON performs the serialization/deserialization of objects using its inbuilt adapters. It also supports custom adapters.
Imagine the API returns a list of family members, which have few different types of members. There are a few dogs, cats and some humans , an there is no particular order.
{
"family":[
{
"id":"5c91012fdbd7835c6720a578",
"members":[
{
"id":"5c91012f57e3c8f1f54499be",
"type":2,
"data":{
"photo":"http://placehold.it/32x32",
"name":"sit",
"tag":{
"id":"5c91012fb0ae1089c92057a4",
"city":"Manchester"
}
}
},
{
"id":"5c91012fb79ec88645ad7f69",
"type":3,
"data":{
"photo":"http://placehold.it/32x32",
"name":"tempor",
"color":"black"
}
},
{
"id":"5c91012fb2e05582cbb207da",
"type":1,
"data":{
"photo":"http://placehold.it/32x32",
"name":"magna",
"sex":"male"
}
},
{
"id":"5c91012fa77bba8d3a2f7e1a",
"type":1,
"data":{
"photo":"http://placehold.it/32x32",
"name":"aliqua",
"sex":"female"
}
}
]
}
],
"total_count":4
}
To parse this , we would :
Parse the JSON and break down each type into subtypes , we have 3 subtypes - Dog , Cat , and Human.
const val HUMAN_TYPE = "human"
const val DOG_TYPE = "dog"
const val CAT_TYPE = "cat"
We need to register our JSON sub types with GSON
private fun initGSON() {
GsonBuilder().registerTypeAdapterFactory(getTypeAdapterFactory())
.create()
}
private fun getTypeAdapterFactory(): RuntimeTypeAdapterFactory<DataT> {
return RuntimeTypeAdapterFactory
.of<DataT>(DataT::class.java, "data_type")
.registerSubtype(DogDataT::class.java, DOG_TYPE)
.registerSubtype(CatDataT::class.java, CAT_TYPE)
.registerSubtype(HumanDataT::class.java, HUMAN_TYPE)
}
For each of these subtypes we have few parameters common and can be abstracted in our base type class :
sealed class Data(
@SerializedName("name")
val name: String = "",
@SerializedName("photo")
val photo: String = "",
@SerializedName("type")
val type: Int
)
This sealed class becomes our base to extend functionality for classifying our types
name
photo
name
and photo
are common for all 3 types , type
becomes our discriminator that our JSON library can parse.
The following classes are extending functionality from Data
data class CatData(val color: String) : Data(type = CAT_TYPE)
data class DogData(val tag: Tag) : Data(type = DOG_TYPE)
data class HumanData(val sex: String) : Data(type = HUMAN_TYPE)
To deserialize our family response JSON , our call site would be :
familyResponse.members.forEach { familyMember ->
when (familyMember.data.type) {
DOG_TYPE -> Log.d(
"TYPEConverter",
"${familyMember.data.name} is dog ${(familyMember.data as DogData).tag}"
)
CAT_TYPE -> Log.d(
"TYPEConverter",
"${familyMember.data.name} is cat ${(familyMember.data as CatData).color}"
)
HUMAN_TYPE -> Log.d(
"TYPEConverter",
"${familyMember.data.name} is human ${(familyMember.data as HumanData).sex} "
)
}
}
Similarly with Moshi :
private fun initMoshi() {
Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(DataT::class.java, "data_type")
.withSubtype(DogDataT::class.java, DOG_TYPE)
.withSubtype(CatDataT::class.java, CAT_TYPE)
.withSubtype(HumanDataT::class.java, HUMAN_TYPE)
)
//if you have more adapters, add them before this line:
.add(KotlinJsonAdapterFactory())
.build()
}
private fun parseData() {
val adapter = moshi.adapter<FamilyResponse>(FamilyResponse::class.java)
val familyResponse = adapter.fromJson(jsonData)
}
The value offered by ad-hoc polymorphism is very closely tied to the language you’re using it in. In other words, it’s not a universal tool but one that’s heavily dependent on how well supported it is in your language. Ad-hoc polymorphism is obviously a critical component of Haskell and it has given rise to high amounts of reuse and elegant abstractions in that language but I’m not sure Kotlin would benefit as much from it.