Click here for chapter one

Click here for chapter 2

Click here for chapter 3

At the end of chapter 3, we introduce the custom Converter, which is based on GsonConverterFactory, but actually Gson does not support Kotlin space security features, which can be summarized as follows: Even if non-empty fields are defined in kotlin Class, Gson parses null fields to bypass the null security check and eventually assigns null values to non-empty fields.

Example:

Data class User(val name: String, val avatar: String, val age: Int) Json {"name": "three ", "age":0}Copy the code

In the example, THE Json does not contain the Avatar field, but the end result is normally parsed without error. However, because there is no avatar field, avatar is null. This results in null values only being required when calling avatar, even if Kotlin prompts you that this field is not null.

What’s the reason?

Fields in a Data class do not have default values, so no null parameter constructs are generated. The null parameter construct is not called during Gson internal parsing (because it is not generated). When it comes to the details of Gson internal parsing, the simple explanation is that Gson parsing has the following priority: 1. NewDefaultConstructor

2. NewDefaultImplementationConstructor (this method are collections of related objects logic, skip.)

3.newUnsafeAllocator

Since there is no empty parameter construction, 1 will not go, and 2 will go 3 because of the above reasons. Is an unsafe operation, is not recommended, specific online query answers.

Solution?

Start with business logic

Since avatar is null, I set it to be null.

data class User( val name: String, val avatar: String? , val age: Int )Copy the code

This is certainly possible, but it has its drawbacks.

What if I have this requirement: When AVATAR is not included in Json, then AVATAR will be the default value in Gson’s parse results.

data class User( val name: String, val avatar: String? = "http:....." , val age: Int )Copy the code

This requirement cannot be met because, as mentioned earlier, the default Avatar field in Json is still null without empty parameter constructs and unsafe initialization object logic is performed.

Start with source code analysis

Since the absence of an empty parameter constructor would cause unsafe object initialization operations to be performed internally within Gson, we should give the empty parameter constructor instead.

There are many empty parameter constructors, but I’ll cover only one

Add default values for all fields

data class User(
    val name: String = "",
    val avatar: String = "",
    val age: Int = 11
)
Copy the code

This generates an empty parameter construct that satisfies the default values, but is cumbersome to write, but can be implemented with the help of plug-ins.

But if we parse the following data

{"name": "avatar":null}Copy the code

Null will still be assigned to the Avatar field, and even if the Avatar is nullable, the need to default to the Json field mentioned earlier cannot be resolved.

So, what to do?

Put up with it or use another parsing library. Examples include Moshi and official product Kotlinx. serialization.

moshi

Let’s take a look at how to use it, and how about support for Kotlin

For details, please check the official document: github.com/square/mosh…

Moshi has two ways of parsing, one that relies on reflection and one that doesn’t rely on reflection, and we’re going to do it without reflection.

val moshi: Moshi = Moshi.Builder().build()
val jsonAdapter: JsonAdapter<User> = moshi.adapter(User::class.java)
val user = jsonAdapter.fromJson(json)
println(user)
Copy the code

It’s a bit more difficult to write than Gson, and the parse result user is also nullable. If we want to use this user, we have to nullate it.

But let’s take a look at the support for Kotlin’s empty security, for example, if we define json as follows

{
  "name": "haha",
  "age": 0,
  "avatar":null
}
Copy the code

User is defined as follows

data class User(
    val name: String,
    val avatar: String,
    val age: Int
)
Copy the code

So what’s going to happen? JsonDataException: non-null value ‘Avatar’ was null

If I set avatar to a default value

data class User( val name: String, val avatar: String = "https..." , val age: Int )Copy the code

What happens when avatar fields are not given in JSON data?

{
  "name": "haha",
  "age": 0
}
Copy the code

Unlike Gson, moshi does a better job of supporting non-null kotlin fields.

However, when I searched some articles, I found some jokes about Moshi.

  1. Empty security for generic scenarios in Kotlin is not supported

    Test the

    data class User<T>( val age: Int = 0, // 0 val avatar: String = "https..." , val name: String, // val data:TCopy the code

    Json as follows

    {"name": "age": 0, "data": null}Copy the code

    So if you have generics, you might want to write it this way

    val moshi: Moshi = Moshi.Builder().build()
    val listOfCardsType: Type = Types.newParameterizedType(
        User::class.java,
        UserData::class.java
    )
    val jsonAdapter: JsonAdapter<User<UserData>> = moshi.adapter(listOfCardsType)
    Copy the code

    JsonDataException: non-null value ‘data_’ (JSON name ‘data’) was null, as expected

    What if Json looks like this? No data field

    {"name": "age": 0}Copy the code

    JsonDataException: Required value ‘data_’ (JSON name ‘data’) missing

    What if it’s a List?

    Json as follows

    [
      null,
      null,
      null
    ]
    Copy the code

    The following code

    val moshi: Moshi = Moshi.Builder().build()
    val listOfCardsType: Type = Types.newParameterizedType(
        List::class.java,
        UserData::class.java
    )
    val jsonAdapter: JsonAdapter<List<UserData>> = moshi.adapter(listOfCardsType)
    val result = jsonAdapter.fromJson(json)
    
    Log.i(TAG, "result: " + result?.get(0))
    Copy the code

    The result is normal, and the log prints result: NULL, still null assigned to the non-null UserData, this is not expected, should throw an exception.

  2. Support for ArrayList and List

    Define the class

    data class User( val age: Int = 0, // 0 val data: List<Data> = listOf(), val name: {data class data (val key: Int = 0 // 1)}Copy the code

    code

    val jsonAdapter: JsonAdapter<User> = moshi.adapter(User::class.java)
    Copy the code

    The result is normal parsing, but an error is reported if the data type is changed to ArrayList

    No JsonAdapter for java.util.ArrayList<User$Data>, you should probably use List instead of ArrayList (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter
    Copy the code

    Either use List or MutableList, or define a custom JsonAdapter to parse the ArrayList.

Summary:

Moshi is a bit more cumbersome, with no support for arrayLists and no support for null values of T in lists. In addition, kotlin’s air security support is still better than Gson’s.

kotlinx.serialization

Address: github.com/Kotlin/kotl…

For the configuration, see….

val user = Json.decodeFromString<User>(json)
Copy the code

The code is simple, one line of code, and the User returned is non-null, which is a bit better than Moshi.

Tests for kotlinx.serialization will not be demonstrated in detail. Kotlinx. Serialization is supported by Moshi, and not by Moshi, for example:

Define json and User as follows

} data class User(val age: Int = 0, // 0 val data: List<Data> = listOf(), val name: String = "";Copy the code

Results as expected, parsing error

kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 45: Expected start of the object '{', but had ' ' instead
Copy the code

ArrayList is also supported.

Summary:

Kotlinx. serialization is a lot easier to use than Moshi. Moshi doesn’t support kotlinx.serialization.


This article is mainly to show the custom Converter in Retrofit, which uses Moshi or Kotlinx. Serialization to replace GsonConverterFactory.


Moshi custom parsing

Let’s start with how to use Moshi in Retrofit.

First is to build a good wheels: com. Squareup. Retrofit2: converter – moshi. Just rely on the latest version.

private val moshi = Moshi.Builder()
    .build()

Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi))
Copy the code

MoshiConverterFactory. Create need to pass in a moshi object, can be initialized directly here, this is the most basic use.

However, remember our chapter 3 business requirements? Let me paste it here again

The request result is

{ "data": [...] , "errorCode": 0, "errorMsg": "" }Copy the code

What we’re trying to achieve

LifecycleScope. Launch {val result = service.banner() // This BizSuccess contains body.errorCode == 0 if (result is ApiResult.BizSuccess) { tvResult.setText(result.data.toString()) } }Copy the code

BizSuccess is returned if the business success condition (errorCode=0) is met.BizError is returned if the business success condition (errorCode=0) is met.

Private val = moshi moshi. Builder () / / custom analytic can increase. The add (MoshiApiResultConverterFactory ()). The build () Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi))Copy the code

We’ve added a MoshiApiResultConverterFactory for handling ApiResult custom resolution, direct post code

class MoshiApiResultConverterFactory : JsonAdapter.Factory { override fun create( type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi ): JsonAdapter<*>? ApiResult<T> val rawType = type.rawType if (rawType! = ApiResult::class.java) return null // Get a generic argument to the ApiResult, such as Banner Val dataType: Type = (Type as? ParameterizedType) ? .actualTypeArguments? .firstOrNull() ? : // Code 1: NextAdapter <Any>(this, dataType, annotations ) return ApiResultTypeAdapter(rawType, dataTypeAdapter) } class ApiResultTypeAdapter<T>( private val outerType: Type, private val dataTypeAdapter: JsonAdapter<T> ) : JsonAdapter<T>() { override fun fromJson(reader: JsonReader): T? Reader.beginobject () var code: Int? = null var msg: String? = null var data: Any? Val nullableStringAdapter: JsonAdapter<String? > = Moshi.Builder().build().adapter(String::class.java, emptySet(), While (reader.hasnext ()) {when (reader.nextname ()) {"code" -> code = reader.nextString().toIntOrNull() "message" -> msg = nullableStringAdapter.fromJson(reader) "data" -> data = dataTypeAdapter.fromJson(reader) else -> reader.skipValue() } } reader.endObject() return if (code ! = 0) ApiResult.BizError( code ? : -1, msg ? : "N/A" ) as T else ApiResult.BizSuccess( code, msg ? : "N/A", data ) as T? } override fun toJson(writer: JsonWriter, value: T?) : Unit = TODO("Not yet implemented") } }Copy the code

The code is also easy to understand, just look at the comments. Where code 1

val dataTypeAdapter = moshi.nextAdapter<Any>(
            this, dataType, annotations
        )
Copy the code

Because every parse class, such as Banner in our case, needs to add an @jSONClass (generateAdapter = True) annotation to generate the corresponding Adapter parse JSON. The nextAdapter() method here is designed much like Retrofit. In short, we got the adapter corresponding to T in the ApiResult, and then used the obtained adapter to parse the data.

Notice what responseType() in the ApiResultCallAdapter is passed in Chapter 3.

ApiResult, not T. Otherwise unable to match to our custom MoshiApiResultConverterFactory

Kotlinx. serialization Custom parsing

Kotlinx-serialization-json :1.3.1 has a problem with custom parses with generic classes, so we failed to compile the custom parser ApiResult.

Related questions are linked below

Here’s my problem: github.com/Kotlin/kotl…

Could this point be kotlin-Kapt’s problem: github.com/Kotlin/kotl…

The SEALED ApiResult may also report an error when customizing a resolution.

Back to business, there is a kotlinx-serialization-Converter for us to use, from the JakeWharton giant at github.com/JakeWharton…

Common use is simple, as follows:

val contentType = "application/json".toMediaType()

Retrofit.Builder().addConverterFactory(Json.asConverterFactory(contentType))
Copy the code

Since the code didn’t run and couldn’t be validated, and I used the Banner< T > instead of the ApiResult< T >, I can only say that the custom resolution of the Banner without the generic was successful. But Banner< T > and ApiResult< T > with generics only look at pseudocode.

The following Json is given, which is also the result of wanAndroid’s Banner interface

{" data ": [{" desc" : "together to make a App", "id" : 10, "imagePath" : "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png", "isVisible": 1, "order": 1, "title": "Together to make a App", "type" : 0, "url" : "https://www.wanandroid.com/blog/show/2"}], "errorCode" : 0, "errorMsg" : ""}Copy the code

Define the following beans

/ / CustomBannerSerializer specifies that the custom parsing class @ Serializable (with = CustomBannerSerializer: : class) data class Banner < T > ( // Here I define a List<T> generic to demonstrate, although we know that T refers to the Banner.Data val Data: List<T>? = listOf(), val errorCode: Int = 0, // 0 val errorMsg: String = "" ) { data class Data( val desc: Int id = 0, // 10 val imagePath: String = "", // https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png val isVisible: Int = 0, // 1 val order: Int = 0, // 1 val title: String = "" String = "" // https://www.wanandroid.com/blog/show/2 ) }Copy the code

CustomBannerSerializer is used to resolve banners

class CustomBannerSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Banner<T>> { override fun serialize(encoder: Encoder, value: Banner<T>) {} override fun deserialize(decoder: Decoder): Banner<T> { return decoder.decodeStructure(descriptor) { var data:List<T>? = null var errorCode: Int = 0 var errorMsg: String = "while (true) {when (val index = decodeElementIndex(descriptor)) {// Because the index 0 in the Banner is data 0 -> data = decoder.decodeSerializableValue(ListSerializer<T>(dataSerializer)) 1 -> errorCode = decodeIntElement(descriptor, 1) 2 -> errorMsg = decodeStringElement(descriptor, 2) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index")}} Banner(data,errorCode,errorMsg). Also {println(it)}}} SerialDescriptor = dataSerializer.descriptor }Copy the code

We changed the original ApiResult to Banner, but the logic is the same, except that the ApiResult is sealed.

Kotlinx-serialization is a new version of kotlinx-serialization.

The code has been uploaded to Github: github.com/lt19931203/…

Reference links:

1. Blog. Yujinyan. Me/posts/kotli…

2. blog.csdn.net/taotao11012…