json 解析框架,很容易想到 Gson、fastJson 等。而这些流行框架对 kotlin 的支持并不好,而Moshi 天生对 kotlin 友好。
前言
Gson 通过反射反序列化数据,Java 类默认有无参构造函数,对于默认参数能够很好的支持。对于 kotlin ,我们经常使用的 data class
,其往往没有无参构造函数,Gson 便会通过 UnSafe 的方式创建实例,成员无法正常初始化默认值。为了勉强能用,只能将构造参数都加上默认值才行,不过这种兼容方式太过隐晦,有潜在的维护风险。
另外,Gson 无法支持 kotlin 空安全特性。定义为不可空且无默认值的字段,在没有该字段对应的 json 数据时会被赋值为 null,这可能导致使用时引发空指针问题。
Moshi
Moshi 是一个适用于 Android、Java 和 Kotlin 的现代 JSON 库。它可以轻松地将 JSON 解析为 Java 和 Kotlin 类。
kotlin
val json: String = ...
val moshi: Moshi = Moshi.Builder().build()
val jsonAdapter: JsonAdapter<Person> = moshi.adapter<Person>()
val person = jsonAdapter.fromJson(json)
通过类型适配器 JsonAdapter 可以对数据类型 T 进行序列化/反序列化操作,即 toJson
和 fromJson
方法。
内置类型适配器
moshi 内置支持以下类型的类适配器:
- 基本类型
- Arrays, Collections, Lists, Sets, Maps
- Strings
- Enums
直接或间接由它们构成的自定义数据类型都可以直接解析。
反射 OR 代码生成
moshi 支持反射和代码生成两种方式进行 Json 解析。
反射的好处是无需对数据类做任何变动,可以解析 private 和 protected 成员,缺点是引入反射相关库,包体积增大2M多,且反射在性能上稍差。
代码生成的好处是速度更快,缺点是需要对数据类添加注解,无法处理 private 和 protected 成员,用于编译时生成代码,影响编译速度,且注解使用越来越多生成的代码也会越来越多。
反射方案依赖:
groovy
implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
代码生成方案依赖(ksp):
groovy
plugins {
id("com.google.devtools.ksp").version("1.6.10-1.0.4") // Or latest version of KSP
}
dependencies {
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.14.0")
}
使用代码生成,需要使用注解 @JsonClass(generateAdapter = true)
修饰数据类:
kotlin
@JsonClass(generateAdapter = true)
data class Person(
val name: String
)
使用反射时,需要添加 KotlinJsonAdapterFactory
到 Moshi.Builder
:
kotlin
val moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()
💡 注意:这里要使用 addLast 添加 KotlinJsonAdapterFactory
,因为 Adapter 是按添加顺序排列和使用的,如果有自定义的 Adapter,为确保自定义的始终在前,建议通过 addLast
将 KotlinJsonAdapterFactory
始终放在最后。
我们目前使用的是反射方案,主要考虑到侵入性低,数据类几乎无改动。
其实也可以两种方案都使用,Moshi 会优先使用代码生成的 Adapter,没有的话则走反射。
解析 JSON 数组
对于 json 数据:
json
[
{
"rank": "4",
"suit": "CLUBS"
},
{
"rank": "A",
"suit": "HEARTS"
}
]
解析:
java
String cardsJsonResponse = ...;
Type type = Types.newParameterizedType(List.class, Card.class);
JsonAdapter<List<Card>> adapter = moshi.adapter(type);
List<Card> cards = adapter.fromJson(cardsJsonResponse);
和 Gson 类似,为了运行时获取泛型信息,稍微麻烦点,可以定义扩展函数简化用法:
kotlin
inline fun <reified T> Moshi.listAdapter(): JsonAdapter<List<T>> {
val type = Types.newParameterizedType(List::class.java, T::class.java)
return adapter(type)
}
简化后:
kotlin
String cardsJsonResponse = ...
val cards = moshi.listAdapter<Card>().fromJson(cardsJsonResponse)
自定义字段名
如果Json 中字段名和数据类中字段名不一致,或 json 中有空格,可以使用 @Json
注解修饰别名。
json
{
"username": "jesse",
"lucky number": 32
}
kotlin
class Player {
val username: String
@Json(name = "lucky number") val luckyNumber: Int
...
}
忽略字段
使用 @Json(ignore = true)
可以忽略字段的解析,java 中的 @Transient
注解也可以。
kotlin
class BlackjackHand(...) {
@Json(ignore = true)
var total: Int = 0
...
}
Java 支持
Moshi 同样支持 Java。需要注意的是,和 Gson 一样,Java 类需要有无参构造方法,否则成员变量的默认值无法生效。
kotlin
public final class BlackjackHand {
private int total = -1;
...
public BlackjackHand(Card hidden_card, List<Card> visible_cards) {
...
}
}
如上,total 的默认值会为 0.
另外,和 Gson 不一样的是,Moshi 并不支持 JsonElement 这种中间产物,它只支持内置类型如 List、Map。
自定义 JsonAdapter
如果 json 的数据格式和我们想要的不一样,就需要我们自定义 JsonAdapter 来解析了。有意思的是,任何拥有 @Json
和 @ToJson
注解的类都可以成为 Adapter,无需继承 JsonAdapter。
例如 json 格式:
json
{
"title": "Blackjack tournament",
"begin_date": "20151010",
"begin_time": "17:04"
}
目标数据类定义:
kotlin
class Event(
val title: String,
val beginDateAndTime: String
)
我们希望 json 中日期 begin_date 和时间 begin_time 组成 beginDateAndTime 字段。moshi 支持我们在 json 和目标数据转换间定义一个中间类,json 和中间类转换后再转换为最终类型。
定义中间类型,本例中即和 json 匹配的数据类型:
kotlin
class EventJson(
val title: String,
val begin_date: String,
val begin_time: String
)
定义 Adapter :
kotlin
class EventJsonAdapter {
@FromJson
fun eventFromJson(eventJson: EventJson): Event {
return Event(
title = eventJson.title,
beginDateAndTime = "${eventJson.begin_date} ${eventJson.begin_time}"
)
}
@ToJson
fun eventToJson(event: Event): EventJson {
return EventJson(
title = event.title,
begin_date = event.beginDateAndTime.substring(0, 8),
begin_time = event.beginDateAndTime.substring(9, 14),
)
}
}
将 adapter 注册到 moshi:
kotlin
val moshi = Moshi.Builder()
.add(EventJsonAdapter())
.build()
这样就可以使用 moshi 直接将 json 转换成 Event
了。本质是将 Json 和目标数据的相互转换加了个中间步骤,先转换为中间产物,再转为最终 Json 或数据实例。
@JsonQualifier:自定义字段类型解析
如下 json,color 为十六进制 rgb 格式的字符串:
json
{
"width": 1024,
"height": 768,
"color": "#ff0000"
}
数据类,color 为 Int 类型:
kotlin
class Rectangle(
val width: Int,
val height: Int,
val color: Int
)
Json 中 color 字段类型是 String,数据类同名字段类型为 Int,除了上面介绍的自定义 JsonAdapter 外,还可以自定义同一数据的不同数据类型间的转换。
首先自定义注解:
kotlin
@Retention(RUNTIME)
@JsonQualifier
annotation class HexColor
使用注解修饰字段:
kotlin
class Rectangle(
val width: Int,
val height: Int,
@HexColor val color: Int
)
自定义 Adapter:
kotlin
/** Converts strings like #ff0000 to the corresponding color ints. */
class ColorAdapter {
@ToJson fun toJson(@HexColor rgb: Int): String {
return "#%06x".format(rgb)
}
@FromJson @HexColor fun fromJson(rgb: String): Int {
return rgb.substring(1).toInt(16)
}
}
通过这种方式,同一字段可以有不同的解析方式,可能不多见,但的确有用。
适配器组合
举个例子:
kotlin
class UserKeynote(
val type: ResourceType,
val resource: KeynoteResource?
)
enum class ResourceType {
Image,
Text
}
sealed class KeynoteResource(open val id: Int)
data class Image(
override val id: Int,
val image: String
) : KeynoteResource(id)
data class Text(
override val id: Int,
val text: String
) : KeynoteResource(id)
UserKeynote
是目标类,其中的 KeynoteResource
可能是 Image
或 Text
,具体是哪个需要根据 type
字段来决定。也就是说 UserKeynote 的解析需要 Image 或 Text 对应的 Adapter 来完成,具体是哪个取决于 type 的值。
显然自带的 Adapter 不能满足需求,需要自定义 Adapter。
先看下 Adapter 中签名要求(参见源码 AdapterMethodsFactory.java):
@FromJson
:
kotlin
<any access modifier> R fromJson(JsonReader jsonReader) throws <any>
或
<any access modifier> R fromJson(JsonReader jsonReader, JsonAdapter<any> delegate, <any more delegates>) throws <any>
或
<any access modifier> R fromJson(T value) throws <any>
@ToJson
:
kotlin
<any access modifier> void toJson(JsonWriter writer, T value) throws <any>
或
<any access modifier> void toJson(JsonWriter writer, T value, JsonAdapter<any> delegate, <any more delegates>) throws <any>
或
<any access modifier> R toJson(T value) throws <any>
前面分析了我们需要借助 Image 或 Text 对应的 Adapter,所以使用第二组函数签名:
kotlin
class UserKeynoteAdapter {
private val namesOption = JsonReader.Options.of("type")
@FromJson
fun fromJson(
reader: JsonReader,
imageJsonAdapter: JsonAdapter<Image>,
textJsonAdapter: JsonAdapter<Text>
): UserKeynote {
// copy 一份 reader,得到 type
val newReader = reader.peekJson()
newReader.beginObject()
var type: String? = null
while (newReader.hasNext()) {
if (newReader.selectName(namesOption) == 0) {
type = newReader.nextString()
}
newReader.skipName()
newReader.skipValue()
}
newReader.endObject()
// 根据 type 做解析
val resource = when (type) {
ResourceType.Image.name -> {
imageJsonAdapter.fromJson(reader)
}
ResourceType.Text.name -> {
textJsonAdapter.fromJson(reader)
}
else -> throw IllegalArgumentException("unknown type $type")
}
return UserKeynote(ResourceType.valueOf(type), resource)
}
@ToJson
fun toJson(
writer: JsonWriter,
userKeynote: UserKeynote,
imageJsonAdapter: JsonAdapter<Image>,
textJsonAdapter: JsonAdapter<Text>
) {
when (userKeynote.resource) {
is Image -> imageJsonAdapter.toJson(writer, userKeynote.resource)
is Text -> textJsonAdapter.toJson(writer, userKeynote.resource)
null -> {}
}
}
}
函数接收一个 JsonReader / JsonWriter 以及若干 JsonAdapter,可以认为该 Adapter 由其他多个 Adapter 组合完成。这种委托的思路在 Moshi 中很常见,比如内置类型 List 的解析,便是委托给了 T 的适配器,并重复调用。
限制
- 不要 Kotlin 类继承 Java 类
- 不要 Java 类继承 Kotlin 类
这是官方强调不要做的,如果你那么做了,发现还没问题,不要侥幸,建议修改,毕竟有有维护风险,且会误导其他维护的人以为这样是可靠合理的。