Kotlin deserialization

Posted on By ᵇᵒ

一、data class

data class Somebody(
    val name: String,
    val age: Int,
    val girlFriends: List<String> = listOf("Jane", "Lisa")
)

fun main() {
    val json = """
        {
            "name":"Bob",
            "age":30
        }
    """
    val gson = GsonBuilder().setLenient().create()

    val sb = gson.fromJson(json, Somebody::class.java)
    println("sb=$sb")   // sb=Somebody(name=Bob, age=30, girlFriends=null)
}

尽管我们在 data class 里对 girlFriends 设置了默认值,但是当 json 字符串缺少 girlFriends 字段时,解析出来的结果 girlFriends 依然为 null。 为什么 data class 赋予的默认值没有生效?

追踪 gson 构造对象源码:

/*
 * ConstructorConstructor.get
 */
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
    final Type type = typeToken.getType();
    final Class<? super T> rawType = typeToken.getRawType();
	
    // ··· 省略一些缓存容器相关代码

    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
    if (defaultConstructor != null) {
      return defaultConstructor;
    }

    ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
    if (defaultImplementation != null) {
      return defaultImplementation;
    }

    // finally try unsafe
    return newUnsafeAllocator(type, rawType);
}

gson 在构造对象时有三个流程:

  1. newDefaultConstructor
  2. newDefaultImplementationConstructor
  3. newUnsafeAllocator

第一步,尝试获取了无参的构造函数,如果能够找到,则通过 newInstance 反射的方式构建对象。
没有找到无参构造函数则返回 null,然后走第二步 newDefaultImplementationConstructor,这个方法里面都是一些集合类相关对象的逻辑,直接跳过。
最后走第三步 newUnsafeAllocator,通过 Unsafe 的方法,绕过了构造方法,直接构建了一个对象。

二、data class with default parameterless constructor

data class Somebody(
    val name: String,
    val age: Int,
    val girlFriends: List<String>
) {
    @SuppressWarnings("unused") // Used by GSON
    constructor() : this("Bob", 30, listOf("Jane", "Lisa"))
}

fun main() {
    val json = """
        {
            "name":"Bob",
            "age":30
        }
    """
    val gson = GsonBuilder().setLenient().create()

    val sb = gson.fromJson(json, Somebody::class.java)
    println("sb=$sb")   // sb=Somebody(name=Bob, age=30, girlFriends=[Jane, Lisa])
}

Android Studio 这种 IDE 会提示这里的无参次构造函数从未使用,一定不要听从提示的建议而删掉。

三、data class defines default values for all fields

data class Somebody(
    val name: String = "Bob",
    val age: Int = 30,
    val girlFriends: List<String> = listOf("Jane", "Lisa")
)

fun main() {
    val json = """
        {
            "name":"Bob",
            "age":30
        }
    """
    val gson = GsonBuilder().setLenient().create()

    val sb = gson.fromJson(json, Somebody::class.java)
    println("sb=$sb")   // sb=Somebody(name=Bob, age=30, girlFriends=[Jane, Lisa])
}

data class 所有 fields 都设置默认值后,kotlin compiler 会生成默认的无参构造函数。

如果所有 fields 都设置了默认值,同时又显式声明了无参构造函数,则以无参构造函数中的默认值为准(显然的,毕竟无参构造函数最终调用主构造函数,会覆盖掉主构造函数里的默认值):

data class Somebody(
    val name: String = "Bob",
    val age: Int = 30,
    val girlFriends: List<String> = listOf("Jane", "Lisa")
) {
    constructor() : this("Bob", 30, listOf("Jane", "Daddario"))
}

fun main() {
    val json = """
        {
            "name":"Bob",
            "age":30
        }
    """
    val gson = GsonBuilder().setLenient().create()

    val sb = gson.fromJson(json, Somebody::class.java)
    println("sb=$sb")   // sb=Somebody(name=Bob, age=30, girlFriends=[Jane, Daddario])
}

四、normal class

/**
 * 也可以加上注解 @JvmOverloads 重载构造函数(关键依然是必须给构造函数里所有的 field 赋默认值):
 *
 * class Somebody @JvmOverloads constructor(
 *     val name: String = "Bob",
 *     val age: Int = 30,
 *     val girlFriends: List<String> = listOf("Jane", "Lisa")
 * ) {
 *     override fun toString() = "Somebody(name=$name, age=$age, girlFriends=$girlFriends)"
 * }
 */
class Somebody(
    val name: String = "Bob",
    val age: Int = 30,
    val girlFriends: List<String> = listOf("Jane", "Lisa")
) {
    override fun toString() = "Somebody(name=$name, age=$age, girlFriends=$girlFriends)"
}

fun main() {
    val json = """
        {
            "name":"Bob",
            "age":30
        }
    """
    val gson = GsonBuilder().setLenient().create()

    val sb = gson.fromJson(json, Somebody::class.java)
    println("sb=$sb")   // sb=Somebody(name=Bob, age=30, girlFriends=[Jane, Lisa])
}

or

class Somebody {
    val name: String = "Bob"
    val age: Int = 30
    val girlFriends: List<String> = listOf("Jane", "Lisa")

    override fun toString() = "Somebody(name=$name, age=$age, girlFriends=$girlFriends)"
}

fun main() {
    val json = """
        {
            "name":"Bob",
            "age":30
        }
    """
    val gson = GsonBuilder().setLenient().create()

    val sb = gson.fromJson(json, Somebody::class.java)
    println("sb=$sb")   // sb=Somebody(name=Bob, age=30, girlFriends=[Jane, Lisa])
}

这两种普通类的写法,kotlin compile 都会自动生成默认的无参构造函数。
其实跟 data class 类似,只要保证有参构造函数的 filed 都赋有默认值即可(只不过 data class 主构造函数至少要有一个参数)。

五、deserialization post-processing

  • 完整的 json 字符串:{"name":"Bob","age":30,"girlFriends":["Jane","Lisa"]}
  • 缺少 girlFriends 字段的 json:{"name":"Bob","age":30}
  • 不缺字段但字段 girlFriends 对应值为 null 的 json:{"name":"Bob","age":30,"girlFriends":null}

上文测试代码的 json 串都缺少 girlFriends 这个字段,现在让我们做一点小小的改动,添加上 girlFriends 字段,但是其值设置为 null。
如此一来,以上三种修复方式(带无参构造函数的数据类、所有 fields 都设置默认值的数据类、普通类)又失效了。

接下来,用另外一种方式来解决这两种异常 json 字符串的反序列化空安全问题:

interface IAfterDeserializeAction {
    fun doAfterDeserialize()
}

class DeserializeActionAdapterFactory : TypeAdapterFactory {
    override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
        // 获取其他低优先级Factory创建的DelegateAdapter
        val delegate = gson.getDelegateAdapter(this, type)
        return if (IAfterDeserializeAction::class.java.isAssignableFrom(type.rawType)) {
            // 如果type实现了DeserializeAction,则返回包裹后的TypeAdapter
            object : TypeAdapter<T>() {
                @Throws(IOException::class)
                override fun write(out: JsonWriter?, value: T) = delegate.write(out, value)

                @Throws(IOException::class)
                override fun read(`in`: JsonReader?): T? = delegate.read(`in`).also {
                    (it as? IAfterDeserializeAction)?.doAfterDeserialize()
                }
            }
        } else delegate
    }
}


data class Somebody(
    var name: String,
    var age: Int,
    var girlFriends: List<String>
) : IAfterDeserializeAction {

    override fun doAfterDeserialize() {
        name = name ?: "Bob"
        age = if (age != 0) age else 30
        girlFriends = girlFriends ?: listOf("Jane", "Lisa")
    }
}

fun main() {
    // val json = """
    //     {
    //         "name":"Bob",
    //         "age":30
    //     }
    // """

    val json = """
        {
            "name":"Bob",
            "age":30,
            "girlFriends":null
        }
    """
    val gson = GsonBuilder().setLenient()
        .registerTypeAdapterFactory(DeserializeActionAdapterFactory())
        .create()

    val sb = gson.fromJson(json, Somebody::class.java)
    println("sb=$sb")   // sb=Somebody(name=Bob, age=30, girlFriends=[Jane, Lisa])
}
  • 这种方式的 data class fields 必须声明为 var 可变的,以便于 doAfterDeserialize 中赋值。
  • 需要注意,对于诸如 Int 这样的基本数据类型(Int、Long、Float、Double、Boolean),json缺失字段或者字段值为null 解析出来默认为0(Boolean类型默认为false)。故应该是 age = if (age != 0) age else 30 而不是 age = age ?: 30


对于 Android 开发中常使用的 Retrofit,可参照如下方式接入:

val gson = GsonBuilder().setLenient().registerTypeAdapterFactory(DeserializeActionAdapterFactory()).create()
val retrofit = Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(gson)).build()

六、SerializedName annotation and back field

利用 gson @SerializedName 注解和 kotlin back field 的方式:

data class Somebody(
    @field:SerializedName("name") private val _name: String?,
    @field:SerializedName("age") private val _age: Int,
    @field:SerializedName("girlFriends") private val _girlFriends: List<String>?
) {
    val name: String
        get() = _name ?: "Bob"

    val age: Int
        get() = if (_age != 0) _age else 30

    val girlFriends: List<String>
        get() = _girlFriends ?: listOf("Jane", "Lisa")

    override fun toString() = "Somebody(name=$name, age=$age, girlFriends=$girlFriends)"
}

fun main() {
    val json = """
        {
            "name":"Bob",
            "age":30,
            "girlFriends":null
        }
    """
    val gson = GsonBuilder().setLenient().create()

    val sb = gson.fromJson(json, Somebody::class.java)
    println("sb=$sb")   // sb=Somebody(name=Bob, age=30, girlFriends=[Jane, Lisa])
}

七、gson vs moshi vs kotlinx.serialization

// todo