Moshi:现代 Json 解析库全解析

4,089 阅读6分钟

json 解析框架,很容易想到 Gson、fastJson 等。而这些流行框架对 kotlin 的支持并不好,而Moshi 天生对 kotlin 友好。

相关文章:

Moshi 支持 enum 空安全

Moshi 支持 Int/Long 类型 enum

前言

Gson 通过反射反序列化数据,Java 类默认有无参构造函数,对于默认参数能够很好的支持。对于 kotlin ,我们经常使用的 data class,其往往没有无参构造函数,Gson 便会通过 UnSafe 的方式创建实例,成员无法正常初始化默认值。为了勉强能用,只能将构造参数都加上默认值才行,不过这种兼容方式太过隐晦,有潜在的维护风险。

另外,Gson 无法支持 kotlin 空安全特性。定义为不可空且无默认值的字段,在没有该字段对应的 json 数据时会被赋值为 null,这可能导致使用时引发空指针问题。

Moshi

Moshi 是一个适用于 Android、Java 和 Kotlin 的现代 JSON 库。它可以轻松地将 JSON 解析为 Java 和 Kotlin 类。

val json: String = ...

val moshi: Moshi = Moshi.Builder().build()
val jsonAdapter: JsonAdapter<Person> = moshi.adapter<Person>()

val person = jsonAdapter.fromJson(json)

通过类型适配器 JsonAdapter 可以对数据类型 T 进行序列化/反序列化操作,即 toJsonfromJson 方法。

内置类型适配器

moshi 内置支持以下类型的类适配器:

  • 基本类型
  • Arrays, Collections, Lists, Sets, Maps
  • Strings
  • Enums

直接或间接由它们构成的自定义数据类型都可以直接解析。

反射 OR 代码生成

moshi 支持反射和代码生成两种方式进行 Json 解析。

反射的好处是无需对数据类做任何变动,可以解析 private 和 protected 成员,缺点是引入反射相关库,包体积增大2M多,且反射在性能上稍差。

代码生成的好处是速度更快,缺点是需要对数据类添加注解,无法处理 private 和 protected 成员,用于编译时生成代码,影响编译速度,且注解使用越来越多生成的代码也会越来越多。

反射方案依赖:

implementation("com.squareup.moshi:moshi-kotlin:1.14.0")

代码生成方案依赖(ksp):

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) 修饰数据类:

@JsonClass(generateAdapter = true)
data class Person(
	val name: String
)

使用反射时,需要添加 KotlinJsonAdapterFactoryMoshi.Builder

val moshi = Moshi.Builder()
    .addLast(KotlinJsonAdapterFactory())
    .build()

💡 注意:这里要使用 addLast 添加 KotlinJsonAdapterFactory,因为 Adapter 是按添加顺序排列和使用的,如果有自定义的 Adapter,为确保自定义的始终在前,建议通过 addLastKotlinJsonAdapterFactory 始终放在最后。

我们目前使用的是反射方案,主要考虑到侵入性低,数据类几乎无改动。

其实也可以两种方案都使用,Moshi 会优先使用代码生成的 Adapter,没有的话则走反射。

解析 JSON 数组

对于 json 数据:

[
  {
    "rank": "4",
    "suit": "CLUBS"
  },
  {
    "rank": "A",
    "suit": "HEARTS"
  }
]

解析:

String cardsJsonResponse = ...;
Type type = Types.newParameterizedType(List.class, Card.class);
JsonAdapter<List<Card>> adapter = moshi.adapter(type);
List<Card> cards = adapter.fromJson(cardsJsonResponse);

和 Gson 类似,为了运行时获取泛型信息,稍微麻烦点,可以定义扩展函数简化用法:

inline fun <reified T> Moshi.listAdapter(): JsonAdapter<List<T>> {
    val type = Types.newParameterizedType(List::class.java, T::class.java)
    return adapter(type)
}

简化后:

String cardsJsonResponse = ...
val cards = moshi.listAdapter<Card>().fromJson(cardsJsonResponse)

自定义字段名

如果Json 中字段名和数据类中字段名不一致,或 json 中有空格,可以使用 @Json 注解修饰别名。

{
  "username": "jesse",
  "lucky number": 32
}
class Player {
  val username: String
  @Json(name = "lucky number") val luckyNumber: Int

  ...
}

忽略字段

使用 @Json(ignore = true) 可以忽略字段的解析,java 中的 @Transient 注解也可以。

class BlackjackHand(...) {
  @Json(ignore = true)
  var total: Int = 0

  ...
}

Java 支持

Moshi 同样支持 Java。需要注意的是,和 Gson 一样,Java 类需要有无参构造方法,否则成员变量的默认值无法生效。

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 格式:

{
  "title": "Blackjack tournament",
  "begin_date": "20151010",
  "begin_time": "17:04"
}

目标数据类定义:

class Event(
  val title: String,
  val beginDateAndTime: String
)

我们希望 json 中日期 begin_date 和时间 begin_time 组成 beginDateAndTime 字段。moshi 支持我们在 json 和目标数据转换间定义一个中间类,json 和中间类转换后再转换为最终类型。

定义中间类型,本例中即和 json 匹配的数据类型:

class EventJson(
  val title: String,
  val begin_date: String,
  val begin_time: String
)

定义 Adapter :

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:

val moshi = Moshi.Builder()
    .add(EventJsonAdapter())
    .build()

这样就可以使用 moshi 直接将 json 转换成 Event 了。本质是将 Json 和目标数据的相互转换加了个中间步骤,先转换为中间产物,再转为最终 Json 或数据实例。

@JsonQualifier:自定义字段类型解析

如下 json,color 为十六进制 rgb 格式的字符串:

{
  "width": 1024,
  "height": 768,
  "color": "#ff0000"
}

数据类,color 为 Int 类型:

class Rectangle(
  val width: Int,
  val height: Int,
  val color: Int
)

Json 中 color 字段类型是 String,数据类同名字段类型为 Int,除了上面介绍的自定义 JsonAdapter 外,还可以自定义同一数据的不同数据类型间的转换。

首先自定义注解:

@Retention(RUNTIME)
@JsonQualifier
annotation class HexColor

使用注解修饰字段:

class Rectangle(
  val width: Int,
  val height: Int,
  @HexColor val color: Int
)

自定义 Adapter:

/** 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)
  }
}

通过这种方式,同一字段可以有不同的解析方式,可能不多见,但的确有用。

适配器组合

举个例子:

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 可能是 ImageText ,具体是哪个需要根据 type 字段来决定。也就是说 UserKeynote 的解析需要 Image 或 Text 对应的 Adapter 来完成,具体是哪个取决于 type 的值。

显然自带的 Adapter 不能满足需求,需要自定义 Adapter。

先看下 Adapter 中签名要求(参见源码 AdapterMethodsFactory.java):

@FromJson

<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

<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,所以使用第二组函数签名:

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 类

这是官方强调不要做的,如果你那么做了,发现还没问题,不要侥幸,建议修改,毕竟有有维护风险,且会误导其他维护的人以为这样是可靠合理的。