A sequence.

The other day I wrote an article about using GSON to make data fault-tolerant between serialization and deserialization of JSON. The simplest is to use the @SerializedName annotation to configure multiple different JSON Key values, or use the @Expose annotation to configure some exceptions. For more complex data, TypeAdapter can be used to solve the problem. TypeAdapter can be said to be a silver bullet for GSON parsing JSON, all the complex data parsing and fault tolerance problems can be solved by it. For those who don’t know, check out the previous article “Making JSON Data Fault Tolerant with Gson.”

There are some small partners in the comments and the public account background, in view of the specific data fault tolerance scene, put forward specific problems. In this article, we will unify the answers and provide solutions.

Ii. GSON data fault tolerant instance

As described in the previous article, GSON has provided some simple annotations to do fault tolerant handling of the data. More complex operations require the use of the TypeAdapter. Note that once the TypeAdapter is used, the configuration of the annotations will be inoperative.

2.1 What is a TypeAdapter

A TypeAdapter is an abstract class supported since GSON version 2.1 to take over certain types of serialization and deserialization. The two most important methods of the TypeAdapter are write() and read(), which take over the details of serialization and deserialization, respectively.

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

In short, The TypeAdapter supports streaming, so it’s memory-efficient, but a little inconvenient to use. JsonSerializer and JsonDeserializer read data into the memory and then operate the data. The JsonSerializer and JsonDeserializer require more memory than the TypeAdapter, but the API is clearer to use.

Although TypeAdapter is more memory efficient, the amount of data used by our business interface is usually negligible.

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

2.2 An empty string is converted to 0

GSON itself has some default fault-tolerant mechanisms for some strong conversions. For example, converting the string “18” to the Java integer 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. The JSON type for age, which can be either 18 or “18”, is allowed.

{
	"name":"Chengxiang Ink Shadow"."age":18 // "age":"18"
}
Copy the code

If the server says, “This user did not fill in the age information, so it returns an empty string “”, then the client uses Gson to parse.

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

In this case, if you use the default GSON policy, you will get a Crash.

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

There is no surprise, there is no surprise Crash, then let’s see how to solve such data fault tolerance problem?

Since in this scenario, only deserialization is needed, we implement the JsonDeserializer interface to take over the Int type. 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":" Chengxiang ink shadow ", "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

IntDefaut0Adapter IntDefaut0Adapter IntDefaut0Adapter IntDefaut0Adapter IntDefaut0Adapter IntDefaut0Adapter IntDefaut0Adapter IntDefaut0Adapter IntDefaut0Adapter In this example, the integer 0 is treated as an exception argument.

2.3 Going from NULL, [], or List to List

Some friends are more concerned about the compatibility of JSONObject and JSONArray.

For example, to return a List, the data translated into JSON should be a JSONArray wrapped in square brackets []. But when the list is empty and the server returns the data, it could be anything.

{
	"name":"Chengxiang Ink Shadow"."languages": ["EN"."CN"] // 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?

We added a languages field of type ArrayList to the original User class.

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

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

In this case, you can use the JsonElement’s isJsonArray() method to determine whether the current array is a valid JSONArray, and if not, simply 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

The core is the isJsonArray() method, to determine whether the current is a JSONArray, if so, then the specific resolution. At this point it’s very flexible, you can either deserialize the data directly into a List with 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. It’s ok in either case.

2.4 Retain the original Json string

Looking at this subtitle, you might wonder, what’s the case with keeping the original Json string? The resulting Json data is itself a string, and I’ll tell you more.

For example, the User class defined earlier needs to be stored in the SQLite database, as does the Languages field. When it comes to SQLite, it is of course preferable to use some open source ORM frameworks, and many of the best ORM-SQLite frameworks support one-to-many storage in the form of foreign keys. For example, an article corresponds to multiple comments, and a user message corresponds to multiple language messages.

In this scenario we can of course use the one-to-many form of storage provided by the ORM framework itself. However, if you simply store limited data, such as the user’s languages, as in the current example, using foreign keys is a bit heavy for such simple limited data.

If you can store your LANGUAGES field JSON as a string in SQLite, it will be easy. By flattening a multi-level structure down to a level, all that’s left is to extend the deserialization method, which is transparent to the business.

So if Gson is simply fault-tolerant, would we be able to do that if we defined the parsed 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

The reason for this is that the deserialize() method passes JsonElement, but JsonElement is just an abstract class that will eventually be converted to one of its implementation classes depending on the data. These implementation classes are all final classes — JsonObject, JsonArray, JsonPrimitive, JsonNull — and they’re pretty straightforward in their names, and they represent the JSON data scenarios that don’t make sense, so I won’t go into much more.

When you use Gson, you encounter curly braces {} that generate a JsonObject, and strings that are basic type JsonPrimitive objects, which are resolved differently within Gson, resulting in an IllegalStateException.

So let’s look at how to solve this problem.

Since The TypeAdapter is a silver bullet for Gson parsing and can’t find a solution, use it. The idea continues to be to use JsonDeserializer to take over parsing, this time taking over 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(a){
    val jsonStr = "" "{" name ":" ChengXiang ink film ", "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

In this case, I directly use the standard API org.json package class to parse JSON data, of course, you can also parse through the Gson itself some methods, here is just to provide 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, which have nothing to do with Gson. Gson only acts as a bridge, as if this example is not really useful.

Aside from scenarios where apps are rogue, what about Retrofit? Retrofit can be configured with Gson as a data converter, which completes the deserialization process internally. In this case, with Gson’s TypeAdapter, we do not need to write additional parsing code, the network request to go through a set of logic.

If you feel that adding TypeAdapter to Gson while constructing Retrofit is a bit intrusive, you can use it in conjunction with the @jsonAdapter annotation.

Three. Summary moment

A large part of the fault-tolerant processing of the data returned by the server is actually due to the problem that the data is not consistent at both ends. For developers, to do the external data are not trusted, the client does not believe the data read locally, do not believe the data returned by the server, the server can not believe the data passed by the client. This is called defensive programming.

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

  1. TypeAdapter(including JsonSerializer and JsonDeserializer) is the silver bullet for Gson parsing. You can use it to meet all the customization requirements for Json parsing.
  2. registerTypeAdapter()Methods need to specify specific data types that need to be used if you want to support inheritanceregisterTypeHierarchyAdapter()Methods.
  3. If the data amount is small, JsonSerializer and JsonDeserializer are recommended.
  4. For parsing takeover of an entire Java Bean, you can use@JsonAdapterAnnotation.

That’s it. Leave questions at the end of the tweet.

Did this article help you? Comments, likes and retweets are the biggest support, thank you!


Public account background reply growth “growth”, will get my prepared learning materials, can also reply “group”, learning progress together; You can also respond to “questions” and ask me questions.