Moshi 支持 Int/Long 类型 enum

125 阅读2分钟

前言

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)...

大功告成。