前言
其实关于kotlin下json解析的问题我很早就碰到了,直到现在才有时间整理。Android开发中应用最广的json库当属Gson,毫无疑问它是一个非常成熟的库,但是迁移到Kotlin以后,gson就出现了两个问题,class字段默认值失效,非空类型有可能被赋值为null。实际上这两种情况都是同一个原因,在gson issue #1550中被提及。本文我们从现象->原因->解决方案依次来分析这个问题。
示例代码已上传github https://github.com/ColorfulHorse/JsonTest
现象
所有字段都有默认值的情况
@JsonClass(generateAdapter = true)
data class DefaultAll(
val name: String = "me",
val age: Int = 17
)
fun testDefaultAll() {
val json = """{}"""
val p1 = gson.fromJson(json, DefaultAll::class.java)
println("gson parse json: $p1")
val p2 = moshi.adapter(DefaultAll::class.java).fromJson(json)
println("moshi parse json: $p2")
}
// 结果
// gson parse json: DefaultAll(name=me, age=17)
// moshi parse json: DefaultAll(name=me, age=17)
可以看到这种情况下gson和moshi都没有问题
部分字段有默认值
@JsonClass(generateAdapter = true)
data class DefaultPart(
val name: String = "me",
val gender: String = "male",
val age: Int
)
fun testDefaultPart() {
// 这里必须要有age字段,moshi为了保持空安全不允许age为null
val json = """{"age": 17}"""
val p1 = gson.fromJson(json, DefaultPart::class.java)
println("gson parse json: $p1")
val p2 = moshi.adapter(DefaultPart::class.java).fromJson(json)
println("moshi parse json: $p2")
}
// 结果
// gson parse json: DefaultPart(name=null, gender=null, age=17)
// moshi parse json: DefaultPart(name=me, gender=male, age=17)
这种情况下gson忽略了name字段和gender字段默认值,给非空类型设置了一个null值,这个就不符合预期了。而moshi则没有影响。
原因分析
gson丢失默认值原因
Gson反序列化对象时
先尝试获取无参构造函数
失败则尝试List、Map等情况的构造函数
最后使用Unsafe.newInstance兜底(此兜底不会调用构造函数,导致所有对象初始化代码不会调用)
显然出现这种情况是因为Gson获取类的无参构造函数失败了,所以最后走到了unsafe方案。让我们来看看Kotlin代码对应的java代码,一探究竟。AS tools -> kotlin -> show kotlin bytecode可以查看kotlin编译后的字节码,decompile后可以查看对应的java代码。
所有字段都有默认值
public final class DefaultAll {
@NotNull
private final String name;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public DefaultAll(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
}
// $FF: synthetic method
public DefaultAll(String var1, int var2, int var3, DefaultConstructorMarker var4) {
if ((var3 & 1) != 0) {
var1 = "me";
}
if ((var3 & 2) != 0) {
var2 = 17;
}
this(var1, var2);
}
public DefaultAll() {
this((String)null, 0, 3, (DefaultConstructorMarker)null);
}
}
可以看到这种情况下该类会生成空参构造函数,但是空参构造函数中并没有赋值,而是调用了synthetic method这个额外生成的辅助构造函数对字段赋默认值。synthetic method倒数第二个参数是一个int类型,用于标记哪些字段使用默认值赋值,按字段声明顺序它们对应的flag值为2^n也就是1 2 4 8....
因为存在空参构造函数而且会赋值默认值,所以这种情况下gson使用正常。
部分字段有默认值
public final class DefaultPart {
@NotNull
private final String name;
@NotNull
private final String gender;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final String getGender() {
return this.gender;
}
public final int getAge() {
return this.age;
}
public DefaultPart(@NotNull String name, @NotNull String gender, int age) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(gender, "gender");
super();
this.name = name;
this.gender = gender;
this.age = age;
}
// $FF: synthetic method
public DefaultPart(String var1, String var2, int var3, int var4, DefaultConstructorMarker var5) {
// 最低不为0表示第一个默认值字段在json中无值,需要默认值
if ((var4 & 1) != 0) {
var1 = "me";
}
if ((var4 & 2) != 0) {
var2 = "male";
}
this(var1, var2, var3);
}
}
这种情况下该类并没有生成空参构造函数,所以gson实例化时使用了Unsafe,自然默认值不生效。实际上只有所有字段都有默认值时才会生成空参构造函数。
moshi正常工作原因
Moshi库属于square公司,最初由Jake Wharton主导,他是kotlin的拥趸,不难推测moshi对Kotlin做了兼容,实际上也是这样。
Moshi序列化/反序列化时根据每个类反射创建对应的JsonAdapter,用它来进行具体操作,同时支持使用annotationProcessor编译时预先生成各个类的JsonAdapter,空间换时间提升性能。我们从注解器生成的代码入手,看看它做了什么处理,这里选取DefaultPart类生成的DefaultPartJsonAdapter。
public class DefaultPartJsonAdapter(
moshi: Moshi
) : JsonAdapter<DefaultPart>() {
private val options: JsonReader.Options = JsonReader.Options.of("name", "gender", "age")
private val stringAdapter: JsonAdapter<String> = moshi.adapter(String::class.java, emptySet(),
"name")
private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "age")
@Volatile
private var constructorRef: Constructor<DefaultPart>? = null
public override fun toString(): String = buildString(33) {
append("GeneratedJsonAdapter(").append("DefaultPart").append(')') }
public override fun fromJson(reader: JsonReader): DefaultPart {
var name: String? = null
var gender: String? = null
var age: Int? = null
// 补码32个1
var mask0 = -1
reader.beginObject()
while (reader.hasNext()) {
when (reader.selectName(options)) {
0 -> {
// name字段非空类型,如果json中value为null则抛异常
name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader)
// ...1111 & ...1110 = ...1110,最低位0表示第一个默认字段在json中存在,不需要赋默认
mask0 = mask0 and 0xfffffffe.toInt()
}
1 -> {
gender = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("gender", "gender",
reader)
// ...1110 & ...1101 = ...1100,次低位0表示第二个默认字段在json中存在,不需要赋默认,以此类推
mask0 = mask0 and 0xfffffffd.toInt()
}
2 -> age = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("age", "age", reader)
-1 -> {
// Unknown name, skip it.
reader.skipName()
reader.skipValue()
}
}
}
reader.endObject()
if (mask0 == 0xfffffffc.toInt()) {
// 如果所有默认字段都存在于json中,则忽略默认值直接调用构造函数赋值成json中的值
return DefaultPart(
name = name as String,
gender = gender as String,
// age字段非空,如果在json中没有对应key则抛异常
age = age ?: throw Util.missingProperty("age", "age", reader)
)
} else {
// 如果有默认值的字段在Json中不存在,则传入flag反射调用synthetic构造函数,填充默认值
@Suppress("UNCHECKED_CAST")
val localConstructor: Constructor<DefaultPart> = this.constructorRef ?:
DefaultPart::class.java.getDeclaredConstructor(String::class.java, String::class.java,
Int::class.javaPrimitiveType, Int::class.javaPrimitiveType,
Util.DEFAULT_CONSTRUCTOR_MARKER).also { this.constructorRef = it }
return localConstructor.newInstance(
name,
gender,
age ?: throw Util.missingProperty("age", "age", reader),
mask0,
/* DefaultConstructorMarker */ null
)
}
}
}
做的事情其实也很简单,代码中我写了注释
用一个int记录(字段超过32个使用多个int)默认值字段在将要解析的json中是否存在,从最低位到最高位依次记录第一个到最后一个默认值字段在json中是否有key,0表示存在,1表示不存在
判断是否所有默认字段在json中都有值,若为true则不用管默认值,直接使用json字段生成实例,若为false则反射调用(synthetic构造器只能够反射调用)synthetic构造器实例化对象,synthetic构造器会根据标志位为默认值字段赋值
一言蔽之,Moshi通过遵循Kotlin的机制做到了兼容。
解决方案
分析了这么多,避免默认值无效的方法已经显而易见了
定义类时所有字段都给一个默认值,这样gson就可以正常工作
使用Moshi库
其他问题,Json中value为null的情况
正常情况下后端返回的Json数据中只应该存在Object类型字段为null的情况,但是现实很骨感,不乏String类型/list类型丢过来也是null的情况。
在Java中,null value会覆盖掉默认值,使用时get方法中判空就可以了。
但是在Kotlin中,如果该字段声明为非空类型,使用gson序列化后非空类型字段会被赋予null值,虽然由于空安全检查是在编译器进行不会报异常,但是这明显非常不符合预期。
而Moshi中对这个情况做了处理,非空字段对应的json value为null时抛JsonDataException,对应的key都不存在时也做同样处理
这些处理逻辑看起来都很合情合理,但是实际开发中不可预期的null value情况又确实存在,我们也不太可能将所有字段都声明为可空类型,那么将Json中null value自定义解析成预设值或许是一个比较好的方法。
Gson自定义解析替换null value
Gson自定义解析使用TypeAdapterFactory
或者单TypeAdapter
,下面示例将声明为String和List的字段通过自定义解析器替换Json中null value为空字符串和空list
class GsonDefaultAdapterFactory: TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.type == String::class.java) {
return createStringAdapter()
}
if (type.rawType == List::class.java || type.rawType == Collection::class.java) {
return createCollectionAdapter(type, gson)
}
return null
}
/**
* null替换成空List
*/
private fun <T : Any> createCollectionAdapter(
type: TypeToken<T>,
gson: Gson
): TypeAdapter<T>? {
val rawType = type.rawType
if (!Collection::class.java.isAssignableFrom(rawType)) {
return null
}
val elementType: Type = `$Gson$Types`.getCollectionElementType(type.type, rawType)
val elementTypeAdapter: TypeAdapter<Any> =
gson.getAdapter(TypeToken.get(elementType)) as TypeAdapter<Any>
return object : TypeAdapter<Collection<Any>>() {
override fun write(writer: JsonWriter, value: Collection<Any>?) {
writer.beginArray()
value?.forEach {
elementTypeAdapter.write(writer, it)
}
writer.endArray()
}
override fun read(reader: JsonReader): Collection<Any> {
val list = mutableListOf<Any>()
// null替换为空list
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return list
}
reader.beginArray()
while (reader.hasNext()) {
val element = elementTypeAdapter.read(reader)
list.add(element)
}
reader.endArray()
return list
}
} as TypeAdapter<T>
}
/**
* null 替换成空字符串
*/
private fun <T : Any> createStringAdapter(): TypeAdapter<T> {
return object : TypeAdapter<String>() {
override fun write(writer: JsonWriter, value: String?) {
if (value == null) {
writer.value("")
} else {
writer.value(value)
}
}
override fun read(reader: JsonReader): String {
// null替换为""
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return ""
}
return reader.nextString()
}
} as TypeAdapter<T>
}
}
测试代码
val gson: Gson = GsonBuilder()
.registerTypeAdapterFactory(GsonDefaultAdapterFactory())
.create()
data class Person(
val name: String,
val friends: List<Person>
)
fun testGsonNullValue() {
// 这里必须要有age字段,moshi为了保持空安全不允许age为null
val json = """{"name":null, "friends":null}"""
val p1 = gson.fromJson(json, Person::class.java)
println("gson parse json: $p1")
}
运行结果gson parse json: Person(name=, friends=[])
,符合预期
Moshi自定义解析替换null value
Moshi中通过JsonAdapter
或者JsonAdapterFactory
自定义解析,这边我直接将moshi标准JsonAdapter拿过来进行了修改
public final class MoshiDefaultAdapterFactory {
private MoshiDefaultAdapterFactory() {
}
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override
public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!annotations.isEmpty()) return null;
......
if (type == String.class) return STRING_JSON_ADAPTER;
return null;
}
};
......
static final JsonAdapter<String> STRING_JSON_ADAPTER = new JsonAdapter<String>() {
@Override
public String fromJson(JsonReader reader) throws IOException {
// 替换null为""
if (reader.peek() != JsonReader.Token.NULL) {
return reader.nextString();
}
reader.nextNull();
return "";
}
@Override
public void toJson(JsonWriter writer, String value) throws IOException {
writer.value(value);
}
@Override
public String toString() {
return "JsonAdapter(String)";
}
};
}
替换空null为空list Adapter
/**
* @author greensun
* @date 2021/6/2
* @desc null 转换成空collection 更改自{@link com.squareup.moshi.CollectionJsonAdapter}
* <p>
* 如果字段声明为Collection, json中值为null,kotlin下在声明类型为非空情况下会抛异常,这里给一个空Collection填充
*/
public abstract class MoshiDefaultCollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> {
public static final Factory FACTORY =
new Factory() {
@Override
public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Class<?> rawType = Types.getRawType(type);
if (!annotations.isEmpty()) return null;
if (rawType == List.class || rawType == Collection.class) {
return newArrayListAdapter(type, moshi);
} else if (rawType == Set.class) {
return newLinkedHashSetAdapter(type, moshi);
}
return null;
}
};
private final JsonAdapter<T> elementAdapter;
private MoshiDefaultCollectionJsonAdapter(JsonAdapter<T> elementAdapter) {
this.elementAdapter = elementAdapter;
}
static <T> JsonAdapter<Collection<T>> newArrayListAdapter(Type type, Moshi moshi) {
Type elementType = Types.collectionElementType(type, Collection.class);
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
return new MoshiDefaultCollectionJsonAdapter<Collection<T>, T>(elementAdapter) {
@Override
Collection<T> newCollection() {
return new ArrayList<>();
}
};
}
static <T> JsonAdapter<Set<T>> newLinkedHashSetAdapter(Type type, Moshi moshi) {
Type elementType = Types.collectionElementType(type, Collection.class);
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
return new MoshiDefaultCollectionJsonAdapter<Set<T>, T>(elementAdapter) {
@Override
Set<T> newCollection() {
return new LinkedHashSet<>();
}
};
}
abstract C newCollection();
@Override
public C fromJson(JsonReader reader) throws IOException {
C result = newCollection();
if (reader.peek() == JsonReader.Token.NULL) {
// null 直接返回空collection
reader.nextNull();
return result;
}
reader.beginArray();
while (reader.hasNext()) {
result.add(elementAdapter.fromJson(reader));
}
reader.endArray();
return result;
}
@Override
public void toJson(JsonWriter writer, C value) throws IOException {
writer.beginArray();
for (T element : value) {
elementAdapter.toJson(writer, element);
}
writer.endArray();
}
@Override
public String toString() {
return elementAdapter + ".collection()";
}
}
测试代码
val moshi: Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(MoshiDefaultAdapterFactory.FACTORY)
.add(MoshiDefaultCollectionJsonAdapter.FACTORY)
.build()
@JsonClass(generateAdapter = true)
data class Person(
val name: String,
val friends: List<Person>
)
fun testMoshiNullValue() {
val json = """{"name":null, "friends":null}"""
val p2 = moshi.adapter(Person::class.java).fromJson(json)
println("moshi parse json: $p2")
}
运行结果moshi parse json: Person(name=, friends=[])
,符合预期
总结
最佳实践:
使用Gson给所有定义字段赋值默认值+自定义解析将不可预期的null值过滤
使用Moshi,自定义解析过滤不可预期的null值
关注我获取更多知识或者投稿
原文链接:https://juejin.cn/post/6969841959082917901