In the previous article, I mentioned GSON’s fault tolerance between JSON serialization and deserialization.

The simplest way is to use the @SerializedName annotation to configure multiple different JSON keys, or use @expose to configure some exceptions. More complex data can be solved using TypeAdapter, which is a silver bullet for GSON parsing JSON. All complex data parsing and fault tolerance problems can be solved through TypeAdapter.

GSON data fault tolerant instance

As mentioned in the previous article, GSON has provided some simple annotations to make data fault-tolerant. For more complex operations, TypeAdapter is required. Note that once the TypeAdapter is on, the configuration of the annotations is invalidated.

What is a TypeAdapter

TypeAdapter is an abstract class that takes over the serialization and deserialization of certain types, starting with GSON version 2.1. The two most important TypeAdapter methods are write() and read(), which take over serialization and deserialization, respectively.

If you want to take over either serialization or deserialization separately, you can use the JsonSerializer and JsonDeserializer interfaces. The combined effect of the two interfaces is similar to that of TypeAdapter, but the internal implementation is different.

In a nutshell, TypeAdapter supports streams, so it’s less memory intensive, but a little cumbersome to use. However, JsonSerializer and JsonDeserializer read all data into memory and then perform operations. They consume more memory than TypeAdapter, but their APIS are clearer to use.

TypeAdapter is more memory efficient, but usually the amount of data used by our business interface does not matter much and can be ignored.

Because with TypeAdapter, JsonSerializer and JsonDeserializer requires GsonBuilder. RegisterTypeAdapter () method, so in this article, the takeover of way, Collectively referred to as TypeAdapter takeover.

The empty string goes to 0

GSON itself has some default fault tolerance for strong conversions. For example, converting the string “18” to a Java integer of 18 is supported by default.

For example, I have a User class that records User information.

class User{
    var name = ""
    var age = 0
    override fun toString(a): String {
        return """
            {
                "name":"${name}",
                "age":${age}} "" ".trimIndent()
    }
}
Copy the code

The User class contains two fields, name and age, where the corresponding JSON type of age can be either 18 or “18”, which is allowed.

{
    "name":"Fragrant ink shadow"."age":18 // "age":"18"
}
Copy the code

If the server says that the user has no age information, so it returns an empty string “”, then the client will use Gson to parse.

This is, of course, a server problem. If the data is explicitly Int, then even the default value should be 0 or -1.

But in this case, you still use the default GSON policy to parse, and you get a Crash.

Caused by: com.google.gson.JsonSyntaxException: 
- java.lang.NumberFormatException: 
--  empty String
Copy the code

So let’s see how to solve this data tolerance problem.

In this scenario, only deserialization is required, so we implement the JsonDeserializer interface to take over the Int type. So let’s go straight to the example.

class IntDefaut0Adapter : JsonDeserializer<Int> {
    override fun deserialize(json: JsonElement? , typeOfT:Type? , context:JsonDeserializationContext?).: Int {
        if(json? .getAsString().equals("")) {
            return 0
        }
        try {
            returnjson!! .getAsInt() }catch (e: NumberFormatException) {
            return 0}}}fun intDefault0(a){
    val jsonStr = {"name":" color ", "age":"} "".trimIndent()
    val user = GsonBuilder()
            .registerTypeAdapter(
                    Int: :class.java,
                    IntDefaut0Adapter())
            .create()
            .fromJson<User>(jsonStr,User::class.java)
    Log.i("cxmydev"."user: ${user.toString()}")}Copy the code

In IntDefaut0Adapter, we first determine if the data string is an empty string “”, if it is, we return 0, otherwise we parse it as Int. In this example, the integer 0 is handled as an exception parameter.

Null, [], List to List

JSONObject and JSONArray are compatible with each other.

For example, if you need to return a List, the JSON data should be the JSONArray wrapped in square brackets []. But when the list is empty, the data returned by the server can be anything.

{" name ":" ChengXiang ink film ", "languages" : [" EN ", "CN"] / / / / the ideal data "languages" : "" / /" languages ": null / /" languages ": {}}Copy the code

In the JSON example, the languages field indicates the language that the current user knows. When the language field is not set, the server returns inconsistent data, how compatible?

Add a languages field of type ArrayList<String> to the User class.

var languages = ArrayList<String>()
Copy the code

In Java, List collections implement the List interface, so when we implement JsonDeserializer, we should intercept the List.

In this case, the isJsonArray() method of JsonElement can be used to determine whether the array is currently a valid JSONArray, and if not, to return an empty collection.

class ArraySecurityAdapter:JsonDeserializer<List<*>>{
    override fun deserialize(json: JsonElement, typeOfT: Type? , context:JsonDeserializationContext?).: List<*> {
      
        if(json.isJsonArray()){
            val newGson = Gson()
            return newGson.fromJson(json, typeOfT)
        }else{
            return Collections.EMPTY_LIST
        }
    }
}

fun listDefaultEmpty(a){
    val jsonStr = "" "{" name ":" ChengXiang ink film ", "age" : "18", "languages" : {}} "" ".trimIndent()
    val user = GsonBuilder()
            .registerTypeHierarchyAdapter(
                    List::class.java,
                    ArraySecurityAdapter())
            .create()
            .fromJson<User>(jsonStr,User::class.java)
    Log.i("cxmydev"."user: ${user.toString()}")}Copy the code

At its core is the isJsonArray() method, which determines whether the current array is a JSONArray, and if so, parses it. At this point, you have the flexibility to deserialize the data directly into a List using Gson, or you can deserialize each item individually through a for loop.

It is important to note that if you still want to use Gson to parse, need to create a new Gson object, can’t reuse JsonDeserializationContext directly, otherwise it will cause the recursive call.

There is a detail, in this case, the call is registerTypeHierarchyAdapter () method to register TypeAdapter, it and we mentioned earlier registerTypeAdapter () what’s the difference?

Usually we choose a collection class that implements different data structures, such as ArrayList or LinkedList, depending on the scenario. But registerTypeAdapter () method, which requires us to pass a specific type, meaning that it does not support inheritance, and registerTypeHierarchyAdapter () can support inheritance.

We want to use the List to replace all the List of subclasses, you need to use registerTypeHierarchyAdapter () method, or Java Bean, we only use the List. You can do either of these things.

Keep the original Json string

Looking at this subtitle, you might wonder, what is it like to keep the original Json string? The resulting Json data is itself a string, but LET me elaborate.

For example, the User class defined earlier needs to be stored in the SQLite database, and the languages field also needs to be stored. Speaking of SQLite, some open source ORM frameworks are preferred, and many of the best orM-SQLite frameworks support one-to-many storage via foreign keys. For example, one article corresponds to multiple comments, and one user information corresponds to multiple language information.

In this scenario we can certainly use the one-to-many storage provided by the ORM framework itself. However, if, like the present example, it is only simple to store some limited data, such as the user’s language, this simple limited data, the use of foreign keys is a little too heavy.

At this point, we wondered if it would be easier to store the JSON field as a string in SQLite. To flatten a multilevel structure into one level, all that is left is to extend a deserialization method, which is transparent to the business.

If Gson had simple fault tolerance, would we have done it by defining the parse field type as String?

@SerializedName("languages")
var languageStr = ""
Copy the code

Unfortunately, there’s no way to do this, and if you do, you’ll get an IllegalStateException.

Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 4 column 18 path $.languages
Copy the code

To put it simply, although deserialize() passes JsonElement as an argument, JsonElement is just an abstract class that will eventually be converted into one of its several implementation classes according to the data. These implementation classes are all final classes, JsonObject, JsonArray, JsonPrimitive, and JsonNull. These classes are pretty well named and represent different JSON data scenarios, but I won’t go into details.

With Gson, curly braces {} generate a JsonObject, and strings are JsonPrimitive objects of the basic type. They are parsed differently within Gson, resulting in IllegalStateException.

So let’s see how we can solve this problem.

Since TypeAdapter is the silver bullet for Gson parsing, no solution can be found, so use it. The idea is to use JsonDeserializer to take over the parsing, this time for the entire parsing of the User class.

class UserGsonAdapter:JsonDeserializer<User>{ override fun deserialize(json: JsonElement, typeOfT: Type? , context: JsonDeserializationContext?) : User { var user = User() if(json.isJsonObject){ val jsonObject = JSONObject(json.asJsonObject.toString()) user.name = jsonObject.optString("name") user.age = jsonObject.optInt("age") user.languageStr = jsonObject.optString("languages") user.languages = ArrayList() val languageJsonArray = JSONArray(user.languageStr) for(i in 0 until languageJsonArray.length()){ user.languages.add(languageJsonArray.optString(i)) } } return user } } fun userGsonStr(){ Val jsonStr = """ {"name":" ", "age":"18", "languages":["CN","EN"] } """.trimIndent() val user = GsonBuilder() .registerTypeAdapter( User::class.java, UserGsonAdapter()) .create() .fromJson<User>(jsonStr,User::class.java) Log.i("cxmydev","user: \n${user.toString()}") }Copy the code

I’m using classes in the standard API org.json package to parse json data directly, but you can also use some of the methods provided by Gson itself, just to give you an idea.

The final Log output looks like this:

{" name ":" ChengXiang ink film ", "age" : 18, "languagesJson:"/" CN ", "EN", "languages size:" 2}Copy the code

In this case, the final resolution uses the standard JSONObject and JSONArray classes, and has nothing to do with Gson, which acts as a bridge, as if this example were not really useful.

Setting aside, apps are rogue, but what about Retrofit? Retrofit can configure Gson as the data converter, and the deserialization process is done internally. In this case, with Gson TypeAdapter, we do not need to write additional parsing code, network request to go a set of logic.

If you feel that adding TypeAdapter to Gson is a bit intrusive when building Retrofit, you can use the @JsonAdapter annotation.

summary

A large part of the fault tolerant processing of the data returned by the server is actually caused by the failure of the two ends to ensure data consistency. For developers, external data are not trusted, the client does not trust the data read locally, do not trust the data returned by the server, the server can not trust the data passed by the client. This is called defensive programming.

Without further ado, let’s summarize the content of this article:

  1. TypeAdapter(including JsonSerializer and JsonDeserializer) is the silver bullet for Gson parsing. All Json parsing requirements can be customized through TypeAdapter.
  2. registerTypeAdapter()Methods need to specify specific data types, which you need to use if you want to support inheritanceregisterTypeHierarchyAdapter()Methods.
  3. JsonSerializer and JsonDeserializer are recommended for small data volumes.
  4. Parse takeover for the entire Java Bean, can be used@JsonAdapterAnnotation.