背景
Moshi
支持 enum
枚举,但是解析时如果没有匹配到定义的枚举值便会报错,其实有些时候枚举的值随着业务也会有所变化,比如新增了一个枚举类型,我们当然不希望旧版本解析崩溃不可用。
问题
Mosh 是支持 enum 类型的。如下 json:
{
"type": "Image"
...
}
class UserKeynote(
val type: ResourceType,
...
)
enum class ResourceType {
Image,
Text
}
解析时 Moshi 默认使用 enum 的 name 做匹配,当然也可以通过 @Json
自定义名字。
业务中,ResourceType
是后续可扩展的,比如后续新增一种类型 Audio,旧版本将解析失败。因此我们把 type
声明为可空的:
class UserKeynote(
val type: ResourceType?,
...
)
然后业务上可以考虑怎么处理空类型,忽略或提示用户升级等。
不过,这样的修改 Moshi 并不能在遇到未知的 Audio 时,将 type 字段置为 null。看一下EnumJsonAdapter
fromJson 源码:
@Override
public T fromJson(JsonReader reader) throws IOException {
int index = reader.selectString(options);
if (index != -1) return constants[index];
// We can consume the string safely, we are terminating anyway.
String path = reader.getPath();
String name = reader.nextString();
throw new JsonDataException(
"Expected one of "
+ Arrays.asList(nameStrings)
+ " but was "
+ name
+ " at path "
+ path);
}
constants 中是所有枚举值,如果找不到对应枚举便会抛异常。
自定义 SafeEnumStringJsonAdapter
我们希望在解析 enum 时,如果不存在对应枚举则赋值 null。模仿 EnumJsonAdapter
的实现,定义 SafeEnumStringJsonAdapter
如下:
/**
* 空安全的 Enum 解析器。
* Moshi 自带的 [EnumJsonAdapter] 在遇到不认得的枚举值时会抛出异常,但在业务迭代时枚举并不是保持不变的。
* 如果遇到这种情况,我们希望它能向前兼容不发生异常并返回 null 值,当然这也需要 Enum 的定义是 Nullable 的。
*/
internal class SafeEnumStringJsonAdapter<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 index = reader.selectString(options)
return if (index != -1) {
constants!![index]
} else {
// We can consume the string safely
reader.nextString()
null
}
}
@Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: T?) {
writer.value(nameStrings[value!!.ordinal])
}
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
中不认识的枚举就返回 null。然后定义 SafeEnumJsonAdapterFactory
决定使用时机:
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 SafeEnumStringJsonAdapter(rawType as Class<out Enum<*>>).nullSafe()
}
return null
}
}
创建 Moshi 实例时使用 SafeEnumJsonAdapterFactory
:
Moshi.Builder().add(SafeEnumJsonAdapterFactory)
如此,添加了 SafeEnumJsonAdapterFactory
的 Moshi 实例在解析 enum 时,遇到未知枚举值时将赋值为 null,同时 enum 的声明也必须是可空的。当然了,如果确认业务中的枚举不会变化,也可以定义不可空的,这点和之前一样。
话说,为什么 SafeEnumStringJsonAdapter
名字中有个 String 呢?因为 Moshi 只支持解析 json 中值为 String 的枚举,如果是 Int 或 Long 则无效,所以我还自定义了个 SafeEnumNumberJsonAdapter
,感兴趣的话见我的另一篇文章:Moshi 支持 Int/Long 类型 enum。