Moshi perfectly solves the problem of empty default value of Gson in kotlin in the true sense

Moshi

Moshi is a Json library that is more friendly to Kotlin, square/moshi: A modern JSON library for Kotlin and Java. (github.com)

Depends

implementation("com.squareup.moshi:moshi:1.8.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.8.0")

Usage scenario

Reflection based on kotlin-reflection requires additional com.squareup.moshi:moshi-kotlin:1.13.0 dependency

// generateAdapter = true means use codegen to generate the JsonAdapter of this class
@JsonClass(generateAdapter = true)
// @Json identifies the field name in json
data class Person(@Json(name = "_name")val name: String, val age: Int)
fun main() {
 val moshi: Moshi = Moshi.Builder()
 ? ? ? ?// KotlinJsonAdapterFactory creates a custom type of JsonAdapter based on kotlin-reflection reflection
 ?.addLast(KotlinJsonAdapterFactory())
 ?.build()
 val json = """{"_name": "xxx", "age": 20}"""
 ?val person = moshi.adapter(Person::class.java).fromJson(json)
 println(person)
}
  • KotlinJsonAdapterFactory is used to reflect the JsonAdapter that generates the data class. If codegen is not used, this configuration is necessary; if there are multiple factories, KotlinJsonAdapterFactory is generally added to the end, because when creating an Adapter, the factory is traversed sequentially creation, reflective creation should be used as a last resort

  • @JsonClass(generateAdapter = true) identifies this class, let codegen generate this kind of JsonAdapter at compile time, codegen requires the data class and its properties visibility to be internal/public

  • Moshi does not allow classes that need to be serialized that are not pure Java/Kotlin classes, for example, Java inherits Kotlin or Kotlin inherits Java

Problems

All fields have default values

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

In this case, both gson and moshi can parse “{}” json characters normally

Some fields have default values

@JsonClass(generateAdapter = true)
data class DefaultPart(
 ?val name: String = "me",
 ?val gender: String = "male",
 ?val age: Int
)
?
// For the following json gson ignores the default values of name and gender attributes, and moshi can parse normally
val json = """{"age": 17}"""
?

Reason

When Gson deserializes an object, it first obtains the no-argument constructor. Since the DefaultPart age attribute has no default value, after generating the bytecode file, this class does not have a no-argument constructor. All Gson finally calls the Unsafe.newInstance function, which does not The constructor will be called to execute the object initialization code, resulting in the name and gender objects being null.

Moshi matches the constructor of the class through the adapter, and uses the constructor with the closest function signature to construct the object. The default value of the default value may not be lost, but in the official routine, there will still be some situations that we do not want to appear. question.

Moshi’s special Json scene

1. Missing attributes

for the following classes

@JsonClass(generateAdapter = true)
data class DefaultPart(
 ?val name: String,
 ?val gender: String = "male",
 ?val age: Int
)

If json = “””{“name”:”John”,”age”:18}””” Moshi can parse normally, but if Json=”””{“name”:”John”}”” “Moshi will throw an exception of Required value age missing at $,

2. Attribute=null

If Json = “””{“name”:”John”,”age”:null} “””Moshi will throw an exception of Non-null value age was null at $

In many cases, the Json data returned by the background is not completely unified, and there will be the above situation. We can deal with it by setting the default value for the age attribute such as the gender attribute, but can we be lazy and do not need to write the default value? Can give a default value out.

Perfect Moshi

Analyzing the KotlinJsonAdapterFactory class in the official library, I found that the judgment code of the above two logics is here

internal class KotlinJsonAdapter<T>(
 ?val constructor: KFunction<T>,
 // bindingAdpter for all attributes
 ?val allBindings: List<Binding<T, Any?>?>,
 ? ?// Ignore deserialized properties
 ?val nonIgnoredBindings: List<Binding<T, Any?>>,
 ? ?// The attribute list obtained from the reflection class
 ?val options: JsonReader.Options
) : JsonAdapter<T>() {<!-- -->
?
 ?override fun fromJson(reader: JsonReader): T {<!-- -->
 val constructorSize = constructor.parameters.size
?
 ?// Read each value into its slot in the array.
 val values = Array<Any?>(allBindings. size) {<!-- --> ABSENT_VALUE }
 reader.beginObject()
 while (reader.hasNext()) {<!-- -->
 ? ? ? ?//Get the index of the class attribute corresponding to the Json attribute through the reader
 ?val index = reader. selectName(options)
 ?if (index == -1) {<!-- -->
 reader.skipName()
 reader.skipValue()
 continue
 ? ? }
 ? ? ? ?//Get the binding of this attribute
 ?val binding = nonIgnoredBindings[index]
        // Get the index of the attribute value
 ? ? val propertyIndex = binding.propertyIndex
 ?if (values[propertyIndex] !== ABSENT_VALUE) {<!-- -->
 throw JsonDataException(
 ? ? ? ?"Multiple values for '${<!-- -->binding.property.name}' at ${<!-- -->reader.path}"
 ? ? ? )
 ? ? }
        // Recursive way to initialize attribute values
 ?values[propertyIndex] = binding.adapter.fromJson(reader)
?
 ? ? ? ? // key place 1
 ? ? ? ?// Determine whether the initialized attribute value is null, if it is null, it means that the json string is reflected as age:null
 ? ? ?if (values[propertyIndex] == null & amp; & amp; !binding.property.returnType.isMarkedNullable) {<!-- -->
 ? ? ? ? // Throw a Non-null value age was null at $ exception
 throw Util.unexpectedNull(
 ?binding.property.name,
 ?binding.jsonName,
 ?reader
 ? ? ? )
 ? ? }
 ? }
 reader.endObject()
?
 // key place 2
 ? ? // Initialize the properties that are not in the remaining json
 ?// Confirm all parameters are present, optional, or nullable.
 ? ? ?// Whether to call the full attribute constructor flag
 var isFullInitialized = allBindings. size == constructorSize
 for (i in 0 until constructorSize) {<!-- -->
 ?if (values[i] === ABSENT_VALUE) {<!-- -->
 ? ? ? ? // If it is equal to ABSENT_VALUE, it means that the attribute is not initialized
 when {<!-- -->
 ? ? ? ? ? // If the attribute is missing, that is, the attribute has a default value, this does not need to be processed, and the full attribute constructor flag is false
 constructor.parameters[i].isOptional -> isFullInitialized = false
 ? ? ? ? ? // If the attribute is nullable, this directly assigns a value of null
 ?constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
 ? ?
 ? ? ? ? ? ? // Throw Required value age missing at $ exception
 ? ? ? ? else -> throw Util.missingProperty(
 constructor.parameters[i].name,
 allBindings[i]?.jsonName,
 ? ? ? ? ? reader
 ? ? ? )
 ? ? ? }
 ? ? }
 ? }
?
 ?// Call the constructor using a Map so that absent optionals get defaults.
 val result = if (isFullInitialized) {<!-- -->
 ?constructor.call(*values)
 ? } else {<!-- -->
 constructor.callBy(IndexedParameterMap(constructor.parameters, values))
 ? }
?
 // Set remaining properties.
 for (i in constructorSize until allBindings. size) {<!-- -->
 ?val binding = allBindings[i]!!
 ?val value = values[i]
 ?binding.set(result, value)
 ? }
?
 return result
  }
?
 ?override fun toJson(writer: JsonWriter, value: T?) {<!-- -->
 if (value == null) throw NullPointerException("value == null")
?
 writer.beginObject()
 for (binding in allBindings) {<!-- -->
 ? ? ?if (binding == null) continue // Skip constructor parameters that aren't properties.
?
 ?writer.name(binding.jsonName)
 binding.adapter.toJson(writer, binding.get(value))
 ? }
 writer.endObject()
  }
?

Through code analysis, can the following modifications be made at two key logic points

?
// key point 1
// Determine whether the initialized attribute value is null, if it is null, it means that the json string is reflected as age:null
if (values[propertyIndex] == null & amp; & amp; !binding.property.returnType.isMarkedNullable) {<!-- -->
 ? ?// Throw a Non-null value age was null at $ exception
 //throw Util.unexpectedNull(
 // ?binding.property.name,
 // ?binding.jsonName,
 // ? reader
 //)
 ? ?// age:null Reset to ABSENT_VALUE value, and it will be initialized when the remaining attributes that are not in json are initialized at the end
    values[propertyIndex] = ABSENT_VALUE
}
?
// key point 2
// Initialize the properties that are not in the remaining json
// Confirm all parameters are present, optional, or nullable.
// Whether to call the full attribute constructor flag
var isFullInitialized = allBindings. size == constructorSize
for (i in 0 until constructorSize) {<!-- -->
 if (values[i] === ABSENT_VALUE) {<!-- -->
 ? ? ? ? // If it is equal to ABSENT_VALUE, it means that the attribute is not initialized
 when {<!-- -->
 ? ? ? ? ? // If the attribute is missing, that is, the attribute has a default value, this does not need to be processed, and the full attribute constructor flag is false
 ?constructor.parameters[i].isOptional -> isFullInitialized = false
 ? ? ? ? ? // If the attribute is nullable, this directly assigns a value of null
 ?constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
 ? ?
 ? ? ? ? ? ? // Throw Required value age missing at $ exception
 ?else ->{<!-- -->
 ? ? ? ? ? ? ? //throw Util.missingProperty(
 ? ? ? ? ? ? ? //constructor.parameters[i].name,
 ? ? ? ? ? ? ? ? //allBindings[i]?.jsonName,
 ? ? ? ? ? ? ? ? ? //reader
 ? ? ? ? ? //)
 ? ? ? ? ? ? ? // fill default
 ? ? ? ? ? val index = options.strings().indexOf(constructor.parameters[i].name)
 ? ? ? ? ? ? val binding = nonIgnoredBindings[index]
 ? ? ? ? ? ? val propertyIndex = binding.propertyIndex
                // Initialize the default value for this property
 values[propertyIndex] = fullDefault(binding)
?
 ? ? ? ? }
 ? ? ? }
 ? }
}
?
?
?
private fun fullDefault(binding: Binding<T, Any?>): Any? {<!-- -->
  return when (binding.property.returnType.classifier) {<!-- -->
 ? ? ? ? ?Int::class -> 0
 ? ? ? ? ?String::class -> ""
 ? ? ? ? Boolean::class -> false
 ? ? ? ? ?Byte::class -> 0.toByte()
 ?Char::class -> Char.MIN_VALUE
 ? ? ? ? ?Double::class -> 0.0
 ? ? ? ? ?Float::class -> 0f
 ? ? ? ? ?Long::class -> 0L
 ? ? ? ? ?Short::class -> 0.toShort()
 ? ? ? ? ? // Filter recursive class initialization, which will lead to an infinite loop
 ?constructor.returnType.classifier -> {<!-- -->
 ? ? ? ? ? ? val message =
 ? ? .classifier})"
 ? ? ? ? ? ? throw JsonDataException(message)
 ? ? ? ? }
 is Any -> {<!-- -->
 ? ? ? ? ? ? ? // If it is a collection, initialize [], otherwise it is {} object
 if (Collection::class.java.isAssignableFrom(binding.property.returnType.javaType.rawType)) {<!-- -->
 ? ? ? ? ? ? ? ?binding.adapter.fromJson("[]")
 ? ? ? ? ? ? } else {<!-- -->
 ? ? ? ? ? ? ? ?binding.adapter.fromJson("{}")
 ? ? ? ? ? ? }
 ? ? ? ? }
 ? ? ? ? ? else -> {<!-- -->}
 ? ? ? }
 ? }

Final effect

“””{“name”:”John”,”age”:null} “”” age will be initialized to 0,

“””{“name”:”John”} “”” age will still be 0, even if we do not define the default value of age in the class

even object

@JsonClass(generateAdapter = true)
data class DefaultPart(
 ?val name: String,
 ?val gender: String = "male",
 ?val age: Int,
 val action:Action
)
class Action(val ac: String)

Finally, Action will also generate a value of Action(ac: “”)

data class RestResponse<T>(
    val code: Int,
    val msg: String="",
    val data: T?
) {<!-- -->
    fun isSuccess() = code == 1

    fun checkData() = data != null

    fun successRestData() = isSuccess() & amp; & amp; checkData()

    fun requsetData() = data!!
}
class TestD(val a:Int,val b:String,val c:Boolean,val d:List<Test> ) {<!-- -->
}

class Test(val a:Int,val b:String,val c:Boolean=true)



val s = """
                {
                    "code":200,
                    "msg":"ok",
                    "data":[{"a":0,"c":false,"d":[{"b":null}]}]}
            """.trimIndent()

val a :RestResponse<List<TestD>>? = s.fromJson()


Final a is {“code”:200,”msg”:”ok”,”data”:[{“a”:0,”b”:””,”c”:false,”d”:[{” a”:0,”b”:””,”c”:true}]}]}