原文地址java
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:android
關於Kotlin中對JSON的處理,網上已經有太多的文章和資源了。首先,在Awesome-Kotlin(在github混過的估計都知道這個約定吧)列表裏有一些列JSON處理庫。而後,還有許多像這篇文章同樣,討論關於如何處理Kotlin data classes 和 JSON。這篇文章的做者使用了Moshi,一個對Kotlin支持很是好的庫。使用Kotlin和JSON的最大挑戰是:git
所以:咱們不想使用android的org.json,由於它的功能很是有限,根本沒有映射功能。github
據我所知,爲了使用Kotlin已知的一些特性,例如空安全和默認參數等,全部的第三方庫都是用了kotlin的反射庫。kotlin的反射庫差很少有2MB,這對於移動平臺來講太大了,因此不建議用。json
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.安全
咱們可能沒法使用像Moshi這樣具備集成Kotlin支持的庫,由於咱們已經使用了項目中使用的流行的Gson或Jackson庫。服務器
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.app
本文描述了一種使用普通Gson庫(Kotson只添加語法糖,並無添加額外的功能)和Kotlin數據類的方法,以及實現JSON到具備null-safety和默認值的Kotlin數據類的映射所需的最小開銷。ide
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.
接下來,咱們只須要經過Gson把下面的JSON映射成Article。
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.
正如預期的那樣,當json的附加屬性不屬於數據類時,它們將被忽略。不起做用的是數據類構造函數中的默認參數。此外,徹底不提供值(titleImage)或顯式地讓值爲null (body)仍然會在Article類型的結果對象中致使null值。當咱們考慮到開發人員在使用非空類型時假定爲空安全時,這尤爲糟糕。它將在運行時致使NullPointerException, IDE沒有提示可能的可空性。咱們甚至不會在解析時獲得異常,由於Gson使用不安全的反射,而Java沒有不可空類型的概念。
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.
對於基本類型,咱們能夠依賴於它們的默認值(不存在的Int值爲0,Boolean值爲false)。全部像字符串這樣的對象都須要爲空。不過,有一個更好的解決方案。
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.
我尚未提到的一個部分是徹底缺少使用Gson反序列化所需的註釋,這很是好。可是@SerializedName()註釋可能會幫上忙。
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.
這是什麼?對於每一個基本類型,咱們只是像之前同樣定義它。若是基本數據類型也能夠是null(來自服務器端),咱們能夠像處理其餘屬性同樣處理它。 咱們仍然在構造函數中提供默認值,以防直接實例化對象而不是從JSON實例化對象。如前所述,當從JSON映射它時,這些默認值將不起做用。爲此,咱們基本上讓構造函數參數爲私有支持屬性(以"_"做爲前綴),但仍然讓Gson屬性的名稱與以前相同(使用註釋)。而後,咱們爲每一個具備實名的支持字段提供只讀屬性,並使用custom get() =和Elvis操做符組合來定義默認值或行爲,從而產生不可空的返回值。
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.
顯然,這個解決方案仍然很冗長,而且會帶來冗長Java bean的佔用內存。可是:在我看來,它只對非基本數據類型有用,並且比編寫自定義解析器更容易。
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.
爲了驗證結果,咱們能夠在 init 代碼塊中調用每個須要的屬性。若是支持的屬性爲null,就會拋出異常(更優雅的解決方案,好比讓整個對象變爲null,須要額外的工做)。另外一種方法是使用泛型TypeAdapterFactory進行後期處理,而不是將其放入init塊中。
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.
據我所知,這是將Gson與Kotlin一塊兒使用並實現所描述的行爲的最優雅的方法,也是實現這種行爲的一種很是簡潔的方法(即便能夠自由選擇庫),由於咱們不須要爲它包含Kotlin -reflect庫。雖然未來可能會有更好的解決方案,可是當使用Kotlin Reflect Lite庫和/或Gson添加本地Kotlin支持時。
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.
自5月16日以來,Moshi徹底支持Kotlin與代碼gen的集成,從而再也不須要包含Kotlin -reflect庫。若是可能的話,我建議你換一下。正如您在本文中所看到的,與本文相比,生成的代碼作了一些事情,可是實際上不須要編寫任何代碼。我想個人帖子仍然對每一個使用Gson庫的人有用。