Gson Advanced — Custom Serialization for Simplification (Part 1)

In this blog post, we'll explore how we can customize the Gson serialization of Java objects. There are many reasons why you might want to change the serialization, e.g. simplifying your model to reduce the amount of data sent or removing personal information. Now we'll look into the simplification of an object by implementing a custom serializer. We'll dive into in a second, we don't need to send the full objects to the server anymore. We just need to send the IDs of the objects. If you're interested to learn how you can achieve that, and also learn how to customize the serialization, keep reading!

Of course, this will not be the only post in our Gson series. If you're interested in the other topics, check out our series outline:

Gson Series Overview

Custom Serialization

Let's imagine our app fulfills the following scenario: the app pulls in a list of available merchants from the server. The user can select a subset of that list as his new subscriptions. The app needs to send back the user information and his selection to the server in a network request.

First, we need to create the model classes for the data we're sending back and forth.

Models

The user information will be covered by the following UserSimple class from the getting started blog post:

public class UserSimple {  
    private String name;
    private String email;
    private boolean isDeveloper;
    private int age;
}

Additionally, we'll have a model for the merchant:

public class Merchant {  
    private int Id;
    private String name;

    // possibly more properties
}

When the app pulls in the information from the server, we get a list of merchants. After the user has selected his merchants, we need to send both, the user information and the subset of merchants back. Thus, we extend the user model UserSimple to a more complex UserSubscription class which includes a list of merchants:

public class UserSubscription {  
    String name;
    String email;
    int age;
    boolean isDeveloper;

    // new!
    List<Merchant> merchantList;
}

The Problem

Theoretically, we're good to go now. After the app has set the necessary properties, Gson would create the following JSON:

{
  "age": 26,
  "email": "norman@fs.io",
  "isDeveloper": true,
  "merchantList": [
    {
      "Id": 23,
      "name": "Future Studio"
    },
    {
      "Id": 42,
      "name": "Coffee Shop"
    }
  ],
  "name": "Norman"
}

It might not be as obvious with this small of a model, but if you imagine the merchant model being much more complex, our request JSON gets quite big.

However, this isn't necessary! The server already knows the merchant information. The entire merchant objects are redundant! The server only needs to know the IDs of the merchants the user wants subscribe to.

Simplify with Property Exclusion

The first approach could be to adjust which merchant properties get serialized. As we've learned in the blog post on exclusion strategies, we can change which properties get (de)serialized. Let's change our Merchant class:

public class Merchant {  
    private int Id;

    @Expose(serialize = false)
    private String name;

    // possibly more properties
}

After adding a few annotations, we could reduce the request JSON to this:

{
  "age": 26,
  "email": "norman@fs.io",
  "isDeveloper": true,
  "merchantList": [
    {
      "Id": 23
    },
    {
      "Id": 42
    }
  ],
  "name": "Norman"
}

This gets us quite close to the optimal result. Nevertheless, we still can reduce it a little further. Additionally, this approach might be problematic if the app sends merchant objects to other endpoints when it does need the full object.

Simplify with Custom Serialization As Single Objects

Since the first approach has its limitations, it's time to look at the better solution: custom serialization. We want to limit the serialization of the merchant objects on a request basis. Sounds complicated, but Gson makes it pretty easy.

Let's go through custom serialization step by step. The previous approach without optimization would look the following:

// get the list of merchants from an API endpoint
Merchant futureStudio = new Merchant(23, "Future Studio", null);  
Merchant coffeeShop = new Merchant(42, "Coffee Shop", null);

// create a new subscription object and pass the merchants to it
List<Merchant> subscribedMerchants = Arrays.asList(futureStudio, coffeeShop);  
UserSubscription subscription = new UserSubscription(  
        "Norman",
        "norman@fs.io",
        26,
        true,
        subscribedMerchants);

Gson gson = new Gson();  
String fullJSON = gson.toJson(subscription);  

In order to optimize this, we need to use a custom Gson instance, register a type adapter for the Merchant class, and then call the toJson() method:

GsonBuilder gsonBuilder = new GsonBuilder();

JsonSerializer<Merchant> serializer = ...; // will implement in a second  
gsonBuilder.registerTypeAdapter(Merchant.class, serializer);

Gson customGson = gsonBuilder.create();  
String customJSON = customGson.toJson(subscription);  

Most of the code above should look familiar if you've done the previous tutorials. The only unknown is the registerTypeAdapter() method. It takes two parameters. The first one is the Type of the object that requires a custom serialization. The second parameter is an implementation of the JsonSerializer interface.

Let's do this final step:

JsonSerializer<Merchant> serializer = new JsonSerializer<Merchant>() {  
    @Override
    public JsonElement serialize(Merchant src, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject jsonMerchant = new JsonObject();

        jsonMerchant.addProperty("Id", src.getId());

        return jsonMerchant;
    }
};

As you can see, we type the JsonSerializer and override the serialize method. It gives us the object (as src) that needs to be serialized. The return is a JsonElement. How you create a JsonElement from your src object depends on the situation. In the snipped above we simply created a new, empty JsonObject and added one property with the ID of the merchant.

This serialize callback will be called every time Gson needs to serialize a Merchant object. In our case this would be for every object in the merchantList.

Once we run this code, it would result in the following JSON:

{
  "age": 26,
  "email": "norman@fs.io",
  "isDeveloper": true,
  "merchantList": [
    {
      "Id": 23
    },
    {
      "Id": 42
    }
  ],
  "name": "Norman"
}

As you can see, the result is the exact same as we would achieve with customizing the serialization via annotations. Additionally, the serialization callback gets called for every element in the list. In the next section, we'll customize the serialization of the entire list, and not just single list items.

Simplify with Custom Serialization As List Objects

You've seen the structure of custom serialization in the previous section. Now we'll adjust it to make our request JSON even smaller. The trick is to target the List<Merchant> part of the JSON.

GsonBuilder gsonBuilder = new GsonBuilder();

Type merchantListType = new TypeToken<List<Merchant>>() {}.getType();  
JsonSerializer<List<Merchant>> serializer = ...; // will implement in a second  
gsonBuilder.registerTypeAdapter(merchantListType, serializer);

Gson customGson = gsonBuilder.create();  
String customJSON = customGson.toJson(subscription);  

Since we're now going for the list object, we need to use the TypeToken class. If you're a little unsure why that's necessary or what the TypeToken is used for, you can freshen up your memory by going back to our Java Lists blog post.

Alright, the critical step is to implement the serializer object:

JsonSerializer<List<Merchant>> serializer =  
    new JsonSerializer<List<Merchant>>() {
        @Override
        public JsonElement serialize(List<Merchant> src, Type typeOfSrc, JsonSerializationContext context) {
            JsonObject jsonMerchant = new JsonObject();

            List<String> merchantIds = new ArrayList<>(src.size());
            for (Merchant merchant : src) {
                merchantIds.add("" + merchant.getId());
            }

            String merchantIdsAsString = TextUtils.join(",", merchantIds);

            jsonMerchant.addProperty("Ids", merchantIdsAsString);

            return jsonMerchant;
        }
}

In the serialize() callback we're creating a new JsonObject and add just a single property. That property Ids contains a string with all the merchant IDs.

{
  "age": 26,
  "email": "norman@fs.io",
  "isDeveloper": true,
  "merchantList": {
    "Ids": "23,42"
  },
  "name": "Norman"
}

The advantage is the reduced size of the JSON. Especially with a larger number of merchants this would mean less data to transfer back to your server, which makes your app a little faster and your app user happier.

However, this solution is a little weird. Sending IDs in a concatenated string is not a standard way of doing it. The best way in the JSON world would be to send the merchantList as an array, and not an object.

Simplify with Custom Serialization As List Array

The final optimization is to adjust the serializer to create an array instead of an object. The general approach stays the same, we just have to change the serializer:

JsonSerializer<List<Merchant>> serializer =  
    new JsonSerializer<List<Merchant>>() {
        @Override
        public JsonElement serialize(List<Merchant> src, Type typeOfSrc, JsonSerializationContext context) {
            JsonArray jsonMerchant = new JsonArray();

            for (Merchant merchant : src) {
                jsonMerchant.add("" + merchant.getId());
            }

            return jsonMerchant;
        }
}

As you can see in the snippet above, the difference is that we create a new JsonArray instead of a JsonObject. We can use the standard add() function to add new elements and return that array. This will result in our final, minimized JSON:

{
  "age": 26,
  "email": "norman@fs.io",
  "isDeveloper": true,
  "merchantList": [
    "23",
    "42"
  ],
  "name": "Norman"
}

You've seen in the past couple of sections, customizing the serialization with GSON is fairly simple on the technical side, but not as straight-forward on the logical side. You really have to think about how you want to structure the data you're sending to the server.

Gson is quite flexible and can cover a lot of different cases. Despite its capabilities, there are a few pitfalls.

Common Issues

One common unexpected issue is the (accidental) overwrite of a custom type adapter. If you declare a type adapter for the same type multiple times, Gson will only respect the last registerTypeAdapter() call.

GsonBuilder gsonBuilder = new GsonBuilder();

Type merchantListType = new TypeToken<List<Merchant>>() {}.getType();

JsonSerializer<List<Merchant>> serializerA = ...;  
JsonSerializer<List<Merchant>> serializerB = ...;

gsonBuilder.registerTypeAdapter(merchantListType, serializerA); // will be ignored  
gsonBuilder.registerTypeAdapter(merchantListType, serializerB); // will be used

Gson customGson = gsonBuilder.create();  
String customJSON = customGson.toJson(subscription);  

As you can see in the snippet above, only the last registerTypeAdapter() is relevant. Registering a (de)serializer for the same type multiple times can happen quite easily, if you don't pay attention. Thus, if you're adding a new custom type adapter, double check if you haven't already implemented and registered something for that type somewhere else.

Secondly, a common issue we've seen with new Gson users is the use of Gson's serialize() method within a serializer implementation. Check the following code snippet:

new JsonSerializer<UserSubscription>() {  
    @Override
    public JsonElement serialize(UserSubscription src, Type typeOfSrc, JsonSerializationContext context) {
        JsonElement jsonSubscription = context.serialize(src, typeOfSrc);

        // customize jsonSubscription here

        return jsonSubscription;
    }
}

The idea is pretty clever: when you're customizing the JSON mapping, you often have to map a bunch of properties manually (see the code examples in the previous sections). The approach to call serialize() in the custom serializer does that work for you.

However, be very careful. If the serialize() call has the same type as your custom serializer, you'll end up in an endless loop. The serialize() call will again end you in your custom serializer, which calls serialize() again, and so on …

Outlook

In this blog post, you've learned how you can utilize Gson's custom serialization to minimize the requests JSONs. This can be quite helpful when you're trying to optimize your apps network usage.

However, this is not the only use case for custom serialization. In the next blog post, we'll look at custom serialization one more time. Next time the focus will be on serializing objects which have no or an unfitting default serialization.

If you've feedback or a question, let us know in the comments or on twitter @futurestud_io.

Make it rock & enjoy coding!

Explore the Library

Find interesting tutorials and solutions for your problems.