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:
关于Kotlin中对JSON的处理,网上已经有太多的文章和资源了。首先,在Awesome-Kotlin(在github混过的估计都知道这个约定吧)列表里有一些列JSON处理库。然后,还有许多像这篇文章一样,讨论关于如何处理Kotlin data classes 和 JSON。这篇文章的作者使用了Moshi,一个对Kotlin支持非常好的库。使用Kotlin和JSON的最大挑战是:
- 我们希望用简洁的代码使用Kotlin数据类
- 以null安全的方式使用非空类型
- 通过数据类构造函数创建数据类时,在给定JSON中缺少字段时使用默认值
- 当映射失败时,我们还可能需要显式异常处理(例如需要知道哪个字段映射失败了)
- 我们还希望从JSON到对象的自动映射的开销接近于0,反之也一样
- 在Android平台下,我们还希望APK包尽量小,所以我们希望能做到更少的依赖和更小的库
因此:我们不想使用android的org.json,因为它的功能非常有限,根本没有映射功能。
据我所知,为了使用Kotlin已知的一些特性,例如空安全和默认参数等,所有的第三方库都是用了kotlin的反射库。kotlin的反射库差不多有2MB,这对于移动平台来说太大了,所以不建议用。
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.
本文描述了一种使用普通Gson库(Kotson只添加语法糖,并没有添加额外的功能)和Kotlin数据类的方法,以及实现JSON到具有null-safety和默认值的Kotlin数据类的映射所需的最小开销。
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库的人有用。