前言
Moshi 是一个适用于 Android、Java 和 Kotlin 的现代 JSON 库。 它支持枚举类型解析,但是只支持解析 json 中值为 String 的枚举,如果是 Number 则无效。
问题
前段时间,我们遇到了一个情况,服务端提供的接口字段中枚举值竟然是数字,问了下这是他们之前就制定的使用规范(一直没严格执行),之后都统一都使用 Int 作为枚举值,但这会使 Moshi 的枚举解析失效。
举例 Json:
{
"roomId": 1238749
"type": 1 // 0:主讲移除 1:攻击他人 2:视频涉黄 3:扰乱课堂
...
}
数据类:
data class ReportRequest(
val roomId: Int,
val type: DefendantType
...
) : BaseData()
enum class DefendantType(val reason: String) {
@Json(name = "0")
LecturerKick("主讲移除"),
@Json(name = "1")
Attack("攻击他人"),
@Json(name = "2")
Porn("视频涉黄"),
@Json(name = "3")
Disturb("扰乱课堂")
}
Moshi 本身支持 String 类型的枚举,这里是数字,所以以上写法中 DefendantType
并不能被成功解析。
自定义 SafeEnumNumberJsonAdapter
本来可以参考自带的 EnumJsonAdapter 的实现,不过之前写过一个可以支持枚举解析失败降级为空值的 SafeEnumStringJsonAdapter
(见文章Moshi 支持 enum 空安全 ),也一起支持了,定义 SafeEnumNumberJsonAdapter
:
class SafeEnumNumberJsonAdapter<T : Enum<T>>(private val enumType: Class<T>) : JsonAdapter<T>() {
private val nameStrings: Array<String?>
private val constants: Array<T>?
private var options: JsonReader.Options? = null
@Throws(IOException::class)
override fun fromJson(reader: JsonReader): T? {
val intString = reader.nextString()
val index = nameStrings.indexOf(intString)
return if (index != -1) {
constants!![index]
} else {
null
}
}
@Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: T?) {
writer.value(nameStrings[value!!.ordinal]?.toLong())
}
override fun toString(): String {
return "JsonAdapter(" + enumType.name + ")"
}
init {
try {
constants = enumType.enumConstants
nameStrings = arrayOfNulls(constants!!.size)
for (i in constants.indices) {
val constant = constants[i]
val annotation = enumType.getField(constant.name).getAnnotation(Json::class.java)
val name = annotation?.name ?: constant.name
nameStrings[i] = name
}
options = JsonReader.Options.of(*nameStrings)
} catch (var6: NoSuchFieldException) {
throw AssertionError("Missing field in " + enumType.name, var6)
}
}
}
关键点就在于 fromJson
里 val intString = reader.nextString(),按照 String 类型读出该值然后进行枚举比较,然后在 toJson
时强转为 Long。
显然这个 JsonAdapter 是专用的,需要针对 Json 值为 Number 的特定枚举,因此还需要注解标识,定义注解 EnumNumber
:
@Retention(AnnotationRetention.RUNTIME)
annotation class EnumNumber
自定义 JsonAdapter.Factory
:
object SafeEnumJsonAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
val rawType = Types.getRawType(type)
if (rawType.isEnum) {
return when {
rawType.annotations.any { it.annotationClass == EnumNumber::class } -> {
SafeEnumNumberJsonAdapter(rawType as Class<out Enum<*>>).nullSafe()
}
else -> {
SafeEnumStringJsonAdapter(rawType as Class<out Enum<*>>).nullSafe()
}
}
}
return null
}
}
如果是使用 EnumNumber
注解的类就使用 SafeEnumNumberJsonAdapter
解析,否则使用 SafeEnumStringJsonAdapter
解析。(注:SafeEnumStringJsonAdapter
见文章 Moshi 支持 enum 空安全 )
回到之前的问题,数据类需要添加 EnumNumber
注解:
@EnumNumber
enum class DefendantType(val reason: String) {
@Json(name = "0")
LecturerKick("主讲移除"),
@Json(name = "1")
Attack("攻击他人"),
@Json(name = "2")
Porn("视频涉黄"),
@Json(name = "3")
Disturb("扰乱课堂")
}
Moshi 的构建需要添加 SafeEnumJsonAdapterFactory
:
Moshi.Builder().add(SafeEnumJsonAdapterFactory)...
大功告成。