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
- Mapping of Enums
- Mapping of Circular References
- Generics
- Custom Serialization for Simplification (Part 1)
- Changing the Default Serialization with Custom Serialization (Part 2)
- Custom Deserialization Basics
- Custom Instance Creator
- Customizing (De)Serialization via @JsonAdapter
- Custom Deserialization for Calculated Fields
- On-The-Fly-Parsing With Streams
- ProGuard Configuration
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!