
Most elegant way of using Gson + Kotlin with default values and null safety
There are already multiple articles and sources about using Kotlin and JSON. 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. The author uses Moshi, which has easy to use Kotlin support. What the challenge of using Kotlin and JSON boils down to is: 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 normal Gson library (Kotson only adds syntactic sugar, so no added functionality) 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.
What we would optimally want is the following:
data class Article(val title: String = "", val body: String = "", val viewCount: Int = 0, val payWall: Boolean = false, val titleImage: String = "")
Then we just map our example JSON with Gson.
val json = """
{ "title": "Most elegant way of using Gson + Kotlin with default values and null safety",
"body": null,
"viewCount": 9999,
"payWall": false,
"ignoredProperty": "Ignored"
}
"""val article = Gson().fromJson(json, Article::class.java)println(article)
// Expected output:
// Article(title=Most elegant way of using Gson + Kotlin with default values and null safety, body=, viewCount=9999, payWall=false, titleImage=)
What works as expected is that additional properties of the json are ignored when they are not part of the data class. What does NOT work are the default arguments inside the data class constructor. Also, not providing a value at all (titleImage
) or having the value be explicitly null (body
) will still result in null values in the resulting object of type Article
. This is especially awful when we consider the assumption of null-safety by the developer when using non-nullable types. It will result in a NullPointerException
at runtime with no hints by the IDE about possible nullability. We won’t even get an exception while parsing, because Gson uses unsafe reflection and Java has no concept of the non-nullable types.
One way of dealing with this is giving in and making everything nullable:
data class Article(val title: String?, val body: String? = null, val viewCount: Int = 0, val payWall: Boolean = false, val titleImage: String? = null)
For primitive types, we can rely on their default values (non-existing Int
will be 0, Boolean
will be false). All Objects like Strings would need to be nullable. There is a better solution though.
One part I haven’t mentioned yet is the complete lack of annotations needed to deserialize with Gson, which is very nice. But the @SerializedName()
annotation might come to our rescue.
data class Article(
@SerializedName("title") private val _title: String?,
@SerializedName("body") private val _body: String? = "",
val viewCount: Int = 0,
val payWall: Boolean = false,
@SerializedName("titleImage") private val _titleImage: String? = ""
) {
val title
get() = _title ?: throw IllegalArgumentException("Title is required") val body
get() = _body ?: "" val titleImage
get() = _titleImage ?: "" init {
this.title
}}
So what do we have here? For every primitive type, we just define it as before. If the primitive can also be null (from server-side), we can handle it like the other properties.
We still provide the default values inside the constructor, in case we instantiate an object directly and not from JSON. Those will NOT work when mapping it from JSON, as said before. For this, we basically have the constructor arguments be private backing properties (prefixed by an underscore), but still have the name of the property for Gson be the same as before (using the annotation). We then provide a read-only property for each backing field with the real name and use the custom get() =
combined with the Elvis operator to define our default value or behavior, resulting in non-nullable return values.
Obviously, this solution is still verbose and brings back hauting memories of verbose Java beans. But: It’s only needed for non-primitives and still easier than writing custom parser in my opinion.
To validate the resulting object, we call every required property in the init
block. If a backing property is null, an exception will be thrown (more elegant solutions like letting the whole object become null would require additional work). An alternative is to use a generic TypeAdapterFactory for post processing instead of putting it inside the init block.
To my knowledge, this is the most elegant way of using Gson with Kotlin and achieving the described behavior, as well as a pretty lean way of achieving this behavior in general (even with free choice of library), as we don’t need to include the kotlin-reflect
library for it. Though there might be better solutions in the future, when the Kotlin Reflect Lite library is used and / or Gson adds native Kotlin support.
UPDATE MAY 2018
Since May 16, Moshi fully supports Kotlin integration with code gen, removing the need to include the kotlin-reflect
library. If possible, I would recommend you to make the switch. As you can see in this medium post, the generated code does things compared to this post, but without the need to actually write any of it. I guess my post remains useful for everyone bound to using the Gson library.