Moshi,更适合Kotlin的Json库(Gson默认值失效解决)

·  阅读 4836

Moshi起步

Moshi是一个对Kotlin更友好的Json库,由Square团队开发,项目地址github.com/square/mosh… Moshi依托JsonAdapter来解析Json,为每个类生成对应的JsonAdapter,同时也可以自定义JsonAdapter来处理某个类

依赖

implementation("com.squareup.moshi:moshi-kotlin:1.13.0")
复制代码

KAPT支持,用于编译期间生成JsonAdapter

kapt("com.squareup.moshi:moshi-kotlin-codegen:1.13.0")
复制代码

如果不使用moshi codegen,会在运行时通过kotlin-reflection反射生成自定义类的JsonAdapter,一方面增加包大小,同时影响运行时效率;而使用codegen则会在编译间生成JsonAdapter,提升一些运行时性能。

基本使用

// generateAdapter = true 表示使用codegen生成这个类的JsonAdapter
@JsonClass(generateAdapter = true)
// @Json 标识json中字段名
data class Person(@Json(name = "_name")val name: String, val age: Int)
fun main() {
    val moshi: Moshi = Moshi.Builder()
        // KotlinJsonAdapterFactory基于kotlin-reflection反射创建自定义类型的JsonAdapter
        .addLast(KotlinJsonAdapterFactory())
        .build()
    val json = """{"_name": "xxx", "age": 20}"""
    val person = moshi.adapter(Person::class.java).fromJson(json)
    println(person)
}
复制代码
  • KotlinJsonAdapterFactory用于反射生成数据类的JsonAdapter,如果不使用codegen,那么这个配置是必要的;如果有多个factory,一般将KotlinJsonAdapterFactory添加到最后,因为创建Adapter时是顺序遍历factory进行创建的,应该把反射创建作为最后的手段
  • @JsonClass(generateAdapter = true)标识此类,让codegen在编译期生成此类的JsonAdapter,codegen需要数据类和它的properties可见性都是internal/public
  • moshi不允许需要序列化的类不是存粹的Java/Kotlin类,比如说Java继承Kotlin或者Kotlin继承Java

自定义JsonAdapter

很多时候我们需要统一对某个类的某个字段做一些类型转换或者过滤操作,比如时间戳转时间字符串,过滤屏蔽词等等,可以通过自定义JsonAdapter在解析阶段就处理这些问题,来做一个简单实现

  1. 定义注解
/**
 * 屏蔽词注解
 */
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class IllegalFilter
/**
 * 时间戳转日期字符串
 */
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class DateText
/**
 * 颜色值互转注解
 */
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class HexColor
复制代码
  1. 定义JsonAdapter,自定义JsonAdapter最简单的方式就是通过@ToJson@FromJson两个注解标记一个普通类的方法
class IllegalTextAdapter {
    companion object {
        val array = arrayOf(
            "屏蔽词1",
            "屏蔽词2"
        )
    }
    @ToJson
    fun toJson(@IllegalFilter text: String): String {
        // 什么都不做
        return text
    }
    @FromJson
    @IllegalFilter
    fun fromJson(text: String): String {
        // 屏蔽词替换为***
        var res = text
        array.forEach {
            res = res.replace(it, "***")
        }
        return res
    }
}
class DateTextAdapter {
    companion object {
        val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
    }
    @ToJson
    fun toJson(@DateText text: String): Long {
        return sdf.parse(text).time
    }
    @FromJson
    @DateText
    fun fromJson(timestamp: Long): String {
        return sdf.format(timestamp)
    }
}
class HexColorAdapter {
    @ToJson
    fun toJson(@HexColor color: Int): String {
        return String.format("#%06x", color)
    }
    @FromJson
    @HexColor
    fun fromJson(color: String): Int {
        return Integer.parseInt(color.substring(1), 16)
    }
}
复制代码
  1. 使用它
@JsonClass(generateAdapter = true)
data class Article(@IllegalFilter val content: String, @DateText val time: String, @HexColor val textColor: Int)
fun main() {
    val json = """{"content": "屏蔽词1,屏蔽词2", "time": 1647846873, "textColor": "#ffffff"}"""
    val article = json.toBean<Article>()
    println("article: $article")
}
复制代码

为什么要使用Moshi,以及Gson、Fastjson在kotlin中的问题

Android开发中应用最广的json库当属Gson,毫无疑问它是一个非常成熟的库,但是迁移到Kotlin以后,gson就出现了两个问题,class中字段默认值在某些情况下失效非空类型有可能被赋值为null。实际上这两种情况都是同一个原因,在gson issue #1550中被提及。本文我们从现象->原因->解决方案依次来分析这个问题。

现象

所有字段都有默认值的情况

@JsonClass(generateAdapter = true)
data class DefaultAll(
    val name: String = "me",
    val age: Int = 17
)

@Test
fun testDefaultAll() {
    val json = """{}"""
    val p1 = gson.fromJson(json, DefaultAll::class.java)
    printlnR("Gson parse json: $p1")
    val p2 = moshi.adapter(DefaultAll::class.java).fromJson(json)
    printlnR("Moshi parse json: $p2")
    val p3 = JSON.parseObject(json, DefaultAll::class.java)
    printlnR("fastjson parse json: $p3")
}
// 结果
// gson parse json: DefaultAll(name=me, age=17)
// moshi parse json: DefaultAll(name=me, age=17)
// fastjson 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)
    printlnR("Gson parse json: $p1")
    val p2 = moshi.adapter(DefaultPart::class.java).fromJson(json)
    printlnR("Moshi parse json: $p2")
    val p3 = JSON.parseObject(json, DefaultPart::class.java)
    printlnR("fastjson parse json: $p3")
}
// 结果
// gson parse json: DefaultPart(name=null, gender=null, age=17)
// moshi parse json: DefaultPart(name=me, gender=male, age=17)
// fastjson default constructor not found. class basic.DefaultPart
复制代码

这种情况下gson忽略了name字段和gender字段默认值,给非空类型设置了一个null值,这个就不符合预期了;fastjson则直接运行异常,找不到默认构造函数;而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,自然默认值不生效。实际上只有所有字段都有默认值时才会生成空参构造函数。

Fastjson 异常原因

其实异常的原因和gson默认值丢失的原因是一样的,fastjson

moshi正常工作原因

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
      )
    }
  }
}
复制代码

做的事情其实也很简单,代码中我写了注释

  1. 用一个int记录(字段超过32个使用多个int)默认值字段在将要解析的json中是否存在,从最低位到最高位依次记录第一个到最后一个默认值字段在json中是否有key,0表示存在,1表示不存在
  1. 判断是否所有默认字段在json中都有值,若为true则不用管默认值,直接使用json字段生成实例,若为false则反射调用(synthetic构造器只能够反射调用)synthetic构造器实例化对象,synthetic构造器会根据标志位为默认值字段赋值 一言蔽之,Moshi通过遵循Kotlin的机制做到了兼容。

解决方案

分析了这么多,避免默认值无效的方法已经显而易见了

  1. 定义类时所有字段都给一个默认值,这样gson和fastjson就可以正常工作
  1. fastjson 2.x 版本也添加了kotlin支持
  1. 使用Moshi库

其他问题,Json中value为null的情况

正常情况下后端返回的Json数据中只应该存在Object类型字段为null的情况,但是现实很骨感,不乏String类型/list类型丢过来也是null的情况。

  • 在Java中,使用字段时一般在get方法中判空
  • 但是在Kotlin中,如果该字段声明为非空类型,使用gson序列化后非空类型字段会被赋予null值(fastjson会忽略null value使用默认值),虽然由于空安全检查是在编译器进行不会报异常,但是这明显非常不符合预期。
  • 而Moshi中对这个情况做了处理,非空字段对应的json value为null时抛JsonDataException

这些处理逻辑看起来都很合情合理,但是实际开发中不可预期的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() {
    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为非空值

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();
        if(value != null) {
            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=[]),符合预期 用这个方法时需要给可能为空的非内置类型字段赋默认值,内置类型则没有要求,null值会被统一替换成adapter中定义的统一默认值,但是这也意味着class中定义的默认值会失效

Moshi忽略null值,使用默认值

通过上述的方法已经解决了内建类型的null值替换,但是对于我们自己定义的model class类型是没法做null值替换的(枚举自定义类型判断不现实)。所以还有另一种思路,就是解析json时直接跳过null值,相当于从json中去掉null字段,这样就会默认使用model class中定义的默认值构造实例,相关方案在moshi issue #843中提出。官方表示此方法不会合并到库中,需要的开发者自行实现,下面贴一下代码。

package com.squareup.moshi
class DefaultIfNullFactory : JsonAdapter.Factory {
    override fun create(
        type: Type,
        annotations: MutableSet<out Annotation>,
        moshi: Moshi
    ): JsonAdapter<*>? {
        val delegate = moshi.nextAdapter<Any>(this, type, annotations)
        if (!annotations.isEmpty()) return null
        if (type === Boolean::class.javaPrimitiveType) return delegate
        if (type === Byte::class.javaPrimitiveType) return delegate
        if (type === Char::class.javaPrimitiveType) return delegate
        if (type === Double::class.javaPrimitiveType) return delegate
        if (type === Float::class.javaPrimitiveType) return delegate
        if (type === Int::class.javaPrimitiveType) return delegate
        if (type === Long::class.javaPrimitiveType) return delegate
        if (type === Short::class.javaPrimitiveType) return delegate
        if (type === Boolean::class.java) return delegate
        if (type === Byte::class.java) return delegate
        if (type === Char::class.java) return delegate
        if (type === Double::class.java) return delegate
        if (type === Float::class.java) return delegate
        if (type === Int::class.java) return delegate
        if (type === Long::class.java) return delegate
        if (type === Short::class.java) return delegate
        if (type === String::class.java) return delegate
        return object : JsonAdapter<Any>() {
            override fun fromJson(reader: JsonReader): Any? {
                return if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) {
                    delegate.fromJson(JsonReaderSkipNullValuesWrapper(reader))
                } else {
                    delegate.fromJson(reader)
                }
            }
            override fun toJson(writer: JsonWriter, value: Any?) {
                return delegate.toJson(writer, value)
            }
        }
    }
}
class JsonReaderSkipNullValuesWrapper(
    private val wrapped: JsonReader
) : JsonReader() {
    private var ignoreSkipName = AtomicBoolean(false)
    private var ignoreSkipValue = AtomicBoolean(false)
    override fun close() {
        wrapped.close()
    }
    override fun beginArray() {
        wrapped.beginArray()
    }
    override fun endArray() {
        wrapped.endArray()
    }
    override fun beginObject() {
        wrapped.beginObject()
    }
    override fun endObject() {
        wrapped.endObject()
        ignoreSkipName.compareAndSet(true, false)
        ignoreSkipValue.compareAndSet(true, false)
    }
    override fun hasNext(): Boolean {
        return wrapped.hasNext()
    }
    override fun peek(): Token {
        return wrapped.peek()
    }
    override fun nextName(): String {
        return wrapped.nextName()
    }
    override fun selectName(options: Options): Int {
        val index = wrapped.selectName(options)
        return if (index >= 0 && wrapped.peek() == Token.NULL) {
            wrapped.skipValue()
            ignoreSkipName.set(true)
            ignoreSkipValue.set(true)
            -1
        } else {
            index
        }
    }
    override fun skipName() {
        if (ignoreSkipName.compareAndSet(true, false)) {
            return
        }
        wrapped.skipName()
    }
    override fun nextString(): String {
        return wrapped.nextString()
    }
    override fun selectString(options: Options): Int {
        return wrapped.selectString(options)
    }
    override fun nextBoolean(): Boolean {
        return wrapped.nextBoolean()
    }
    override fun <T : Any?> nextNull(): T? {
        return wrapped.nextNull()
    }
    override fun nextDouble(): Double {
        return wrapped.nextDouble()
    }
    override fun nextLong(): Long {
        return wrapped.nextLong()
    }
    override fun nextInt(): Int {
        return wrapped.nextInt()
    }
    override fun nextSource(): BufferedSource {
        return wrapped.nextSource()
    }
    override fun skipValue() {
        if (ignoreSkipValue.compareAndSet(true, false)) {
            return
        }
        wrapped.skipValue()
    }
    override fun peekJson(): JsonReader {
        return wrapped.peekJson()
    }
    override fun promoteNameToValue() {
        wrapped.promoteNameToValue()
    }
}
复制代码

需要注意的是用这种方案的时候最好给所有可能返回null的不可空字段定义默认值,否则还是有可能出现null值被跳过导致的空安全异常 另外,这个方法不能和上个方法混用,因为此方法是在read json时就将null剔除出去了,后续不会再进行处理 测试代码

val moshi: Moshi = Moshi.Builder()
    .add(DefaultIfNullFactory())
    .build()
/**
 * 自定义类型为空
 */
@JsonClass(generateAdapter = true)
data class Team(
    val name: String? = "default",
    val leader: Person = Person("default", emptyList())
)
fun testMoshiCustomNullValue() {
    val json = """{"name":null, "leader":null}"""
    val p2 = moshi.adapter(Team::class.java).fromJson(json)
    println("Moshi parse json: $p2")
}
复制代码

运行结果Moshi parse json: Team(name=default, leader=Person(name=default, friends=[])),符合预期

总结

最佳实践:

  1. 使用fastjson,所有字段定义默认值
  1. 使用gson,所有字段定义默认值,自定解析忽略不可预期的null值
  1. 使用Moshi,给所有非空字段定义默认值+自定义解析忽略不可预期的null值
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改