Gson + Kotlin 组合中实现默认值和空安全的最优雅的的方式

原文地址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

  • 咱们但愿用简洁的代码使用Kotlin数据类
  • 以null安全的方式使用非空类型
  • 经过数据类构造函数建立数据类时,在给定JSON中缺乏字段时使用默认值
  • 当映射失败时,咱们还可能须要显式异常处理(例如须要知道哪一个字段映射失败了)
  • 咱们还但愿从JSON到对象的自动映射的开销接近于0,反之也同样
  • 在Android平台下,咱们还但愿APK包尽可能小,因此咱们但愿能作到更少的依赖和更小的库

所以:咱们不想使用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.

2018年5月更新

自5月16日以来,Moshi彻底支持Kotlin与代码gen的集成,从而再也不须要包含Kotlin -reflect库。若是可能的话,我建议你换一下。正如您在本文中所看到的,与本文相比,生成的代码作了一些事情,可是实际上不须要编写任何代码。我想个人帖子仍然对每一个使用Gson库的人有用。

相关文章
相关标签/搜索