Gson Guidelines for Use (4)

Keywords: JSON

Note: This series is based on Gson 2.4.

The main contents of this article are as follows:

  • TypeAdapter
  • Json Serializer and Json Deserializer
  • TypeAdapterFactory
  • @ Json Adapter Annotation
  • Comparison of Type Adapter with Json Serializer and Json Deserializer
  • TypeAdapter instance
  • epilogue
  • Post-warning

Type Adapter

Type Adapter is an abstract class provided by Gson since version 2.0 (source annotations say 2.1) to take over some type of serialization and deserialization process. It contains two annotated methods, write(JsonWriter,T) and read(JsonReader), the other methods are final methods and finally call the two abstract methods.

public abstract class TypeAdapter<T> {
    public abstract void write(JsonWriter out, T value) throws IOException;
    public abstract T read(JsonReader in) throws IOException;
    //Other final methods are not posted, including `toJson', `toJsonTree', `toJson', and `nullSafe'.
}

Note: Type Adapter and Json Serializer and Json Deserializer need to be used in conjunction with GsonBuilder. RegiserType Adapter or GsonBuilder. RegiserType Hierarchy Adapter, which will not be repeated below.

Use examples:

User user = new User("Monster kidou", 24);
user.emailAddress = "ikidou@example.com";
Gson gson = new GsonBuilder()
        //Register TypeAdapter for User
        .registerTypeAdapter(User.class, new UserTypeAdapter())
        .create();
System.out.println(gson.toJson(user));

Definition of UserType Adapter:

public class UserTypeAdapter extends TypeAdapter<User> {

    @Override
    public void write(JsonWriter out, User value) throws IOException {
        out.beginObject();
        out.name("name").value(value.name);
        out.name("age").value(value.age);
        out.name("email").value(value.email);
        out.endObject();
    }

    @Override
    public User read(JsonReader in) throws IOException {
        User user = new User();
        in.beginObject();
        while (in.hasNext()) {
            switch (in.nextName()) {
                case "name":
                    user.name = in.nextString();
                    break;
                case "age":
                    user.age = in.nextInt();
                    break;
                case "email":
                case "email_address":
                case "emailAddress":
                    user.email = in.nextString();
                    break;
            }
        }
        in.endObject();
        return user;
    }
}

When we register the Type Adapter for User.class, as long as we operate on User.class @Serialized Name, Field Naming Strategy, As, Until, Expos, all of them are eclipsed and ineffective, we will only call the User Type Adapter. write (JsonWriter, JsonWriter, JsonWriter, JsonWriter, JsonWriter, JsonWriter, JsonWriter, etc.). Method: Write as I want.

In another scenario, the first article in this series mentioned that Gson has some fault-tolerant mechanisms, such as converting string "24" to int. 24, but what if in some cases an empty string is returned to you (someone commented on this question to me)? Although this is a server-side problem, we're just doing a demonstration here.

Type int will make a mistake, right. According to what we described above, can I register a Type Adapter to take over the serialization and deserialization process?

Gson gson = new GsonBuilder()
        .registerTypeAdapter(Integer.class, new TypeAdapter<Integer>() {
            @Override
            public void write(JsonWriter out, Integer value) throws IOException {
                out.value(String.valueOf(value)); 
            }
            @Override
            public Integer read(JsonReader in) throws IOException {
                try {
                    return Integer.parseInt(in.nextString());
                } catch (NumberFormatException e) {
                    return -1;
                }
            }
        })
        .create();
System.out.println(gson.toJson(100)); // Result: "100"
System.out.println(gson.fromJson("\"\"",Integer.class)); // Results: -1

Note: When testing empty string, it must be """instead of"". It means that there is no JSON string, and""" only means "in json".

You said that this takeover would involve two troubles. I just want to take care of the serialization (or de-serialization) process. I don't care about the other process. Isn't there any simpler way? Of course! Json Serializer and Json Deserializer are introduced next.

Json Serializer and Json Deserializer

Json Serializer and Json Deserializer do not have to implement serialization and deserialization processes like Type Adapter. You can choose to use Json Serializer only to take over the serialization process and Json Deserializer only to take over the deserialization process. Code.

Gson gson = new GsonBuilder()
        .registerTypeAdapter(Integer.class, new JsonDeserializer<Integer>() {
            @Override
            public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
                try {
                    return json.getAsInt();
                } catch (NumberFormatException e) {
                    return -1;
                }
            }
        })
        .create();
System.out.println(gson.toJson(100)); //Result: 100
System.out.println(gson.fromJson("\"\"", Integer.class)); //Result-1

Here's an example of how all numbers are serialized into strings

JsonSerializer<Number> numberJsonSerializer = new JsonSerializer<Number>() {
    @Override
    public JsonElement serialize(Number src, Type typeOfSrc, JsonSerializationContext context) {
        return new JsonPrimitive(String.valueOf(src));
    }
};
Gson gson = new GsonBuilder()
        .registerTypeAdapter(Integer.class, numberJsonSerializer)
        .registerTypeAdapter(Long.class, numberJsonSerializer)
        .registerTypeAdapter(Float.class, numberJsonSerializer)
        .registerTypeAdapter(Double.class, numberJsonSerializer)
        .create();
System.out.println(gson.toJson(100.0f));//Result: "100.0"

Note: RegiserType Adapter must use packaging type, so int.class,long.class,float.class and double.class are not feasible. At the same time, we can't use the parent class to replace the subtype above, which is why we should register separately instead of using Number.class directly.

The above shows that registerType Adapter can't work, that is, there are other ways to do it? Of course! Instead of registerType Hierarchy Adapter, you can use Number.class instead of registering one by one.

The difference between registerType Adapter and registerType Hierarchy Adapter:

  registerTypeAdapter registerTypeHierarchyAdapter
Supporting generics yes no
Supporting inheritance no yes

Note: If a serialized object itself has a generic type and registers the corresponding Type Adapter, then Gson.toJson(Object,Type) must be called to explicitly tell the type of the Gson object.

Type type = new TypeToken<List<User>>() {}.getType();
TypeAdapter typeAdapter = new TypeAdapter<List<User>>() {
   //slightly
};
Gson gson = new GsonBuilder()
        .registerTypeAdapter(type, typeAdapter)
        .create();
List<User> list = new ArrayList<>();
list.add(new User("a",11));
list.add(new User("b",22));
//Notice that there is an additional type parameter
String result = gson.toJson(list, type);

Type Adapter Factory

Type Adapter Factory, as the name implies, is used to create a factory class of Type Adapter. By comparing Type, we can determine whether there is a corresponding Type Adapter. If not, we return null and use it in conjunction with GsonBuilder. RegiserType Adapter Factory.

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(new TypeAdapterFactory() {
        @Override
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            return null;
        }
    })
    .create();

IV. Annotation @JsonAdapter

Json Adapter is more special than the previous annotations of Serialized Name, Field Naming Strategy, Since, Until, Expos. The other annotations are used in POJO fields. This one is used in POJO classes and receives a parameter. It must be Type Adpater, Json Serializer or Json Deserializer. One of the three.

It is said that Json Serializer and Json Deserializer should cooperate with GsonBuilder. RegiserType Adapter, but it is too troublesome to register every time. Json Adapter is to solve this pain point.

How to use it (for example, User):

@JsonAdapter(UserTypeAdapter.class) //Add to Class
public class User {
    public User() {
    }
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
    public String name;
    public int age;
    @SerializedName(value = "emailAddress")
    public String email;
}

No longer use GsonBuilder to register UserType Adapter.
Note: @JsonAdapter only supports Type Adapter or Type Adapter Factory

Gson gson = new Gson();
User user = new User("Monster kidou", 24, "ikidou@example.com");
System.out.println(gson.toJson(user));
//Result: {"name": "kidou","age":24,"email":"ikidou@example.com"}
//To distinguish the results, the email field is specifically set differently from the @SerializedName annotation

Note: The priority of JsonAdapter is higher than that of GsonBuilder. RegiserType Adapter.

V. Comparison of Type Adapter with Json Serializer and Json Deserializer

  TypeAdapter JsonSerializer,JsonDeserializer
Introduced version 2.0 1.x
Stream API Support No *, JsonElement needs to be generated in advance
Memory occupancy Small Bigger than TypeAdapter
efficiency high Lower than TypeAdapter
Scope of action Serialization and deserialization Serialization or deserialization

6. Type Adapter Example

Note: Type Adapter here refers to Type Adapter, Json Serializer and Json Deserializer.
Here, the Type Adapter talks about the problem of null strings that may occur when converting numeric values in the form of strings into ints automatically. Here's a description of the needs of another reader:

The data field type returned by the server is not fixed. For example, the successful data request is a List, and the unsuccessful data request is a String type. How can the front end handle it when using generic parsing?

In fact, this problem is mainly caused by the server side. There is no guarantee of data consistency in the interface design. The correct data return posture: the same interface should not change the return type under any circumstances, either do not return, or return empty values, such as null, [], {}.

But here's the solution:
Programme 1:

Gson gson = new GsonBuilder().registerTypeHierarchyAdapter(List.class, new JsonDeserializer<List<?>>() {
    @Override
    public List<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        if (json.isJsonArray()){
            //Here I am responsible for the analysis.
            Gson newGson = new Gson();
            return newGson.fromJson(json,typeOfT);
        }else {
            //Returns an empty List that does not match the interface type
            return Collections.EMPTY_LIST;
        }
    }
}).create();

Programme II:

 Gson gson = new GsonBuilder().registerTypeHierarchyAdapter(List.class, new JsonDeserializer<List<?>>() {
    @Override
    public List<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        if (json.isJsonArray()) {
            JsonArray array = json.getAsJsonArray();
            Type itemType = ((ParameterizedType) typeOfT).getActualTypeArguments()[0];
            List list = new ArrayList<>();
            for (int i = 0; i < array.size(); i++) {
                JsonElement element = array.get(i);
                Object item = context.deserialize(element, itemType);
                list.add(item);
            }
            return list;
        } else {
            //Returns an empty List that does not match the interface type
            return Collections.EMPTY_LIST;
        }
    }
}).create();

Attention should be paid to:

  • The registerTypeHierarchyAdapter method must be used, otherwise it will not be valid for a subclass of List, but if List is used in POJO, registerTypeAdapter can be used.
  • In the case of arrays, we need to create a new Gson instead of using context directly. Otherwise, gson will call our custom Json Deserializer to make recursive calls. Solution 2 does not recreate Gson, so we need to extract the type of E in List < E > and then de-serialize it to register Adap Type for E manually. The case of er.
  • Recommend scheme 2 in terms of efficiency, eliminating the process of re-instantiating Gson and registering other Type Adapters.

Posted by sprinkles on Fri, 05 Jul 2019 10:57:02 -0700