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 类

这是官方着重不要做的,假如你那么做了,发现还没问题,不要侥幸,主张修改,毕竟有有维护危险,且会误导其他维护的人认为这样是可靠合理的。