
Gson or "There and Back"
- Tutorial
Recently, I had to work with the Google Gson library , designed to convert Java objects to JSON text format (serialization) and reverse conversion (deserialization). Often, when working with Gson, the standard library settings are enough, but there are cases (including mine) when you need to customize the conversion processes.
After working with Gson, I decided to write this tutorial, which illustrates the principles of working with the library using an example. The post turned out to be relatively long, but I don’t want to split it because of the logical coherence of the story.
First you need to select some subject area. Say, I don’t know, for some reason, the thought of a detachment of gnomes comes to mind. Actually, why not?

Yes, all the code involved in the article can be found on GitHub: https://github.com/treble-snake/gson.dwarves
Images, except for the class diagram, are borrowed from the Gson series of articles at http: //www.javacreed .com .
So, with the "squad" it is clear - this is a certain set of gnomes. But what about the dwarves themselves? The most important detail characterizing a gnome is, of course, a beard. You can describe the features and classifications of gnome beards for a long time, but for simplicity we define three parameters: whether the gnome has a mustache, if there is a beard, and what color they are. Further, the name and age - where without them. Add something else personal, say the dwarf ate lunch. And finally, weapons. A gnome can have a lot of weapons, and it can be simple, or it can be unique, having its own name and origin.
The result is something like this:

We initialize our gnome company by adding three participants ( all characters are fictitious, and coincidences are random ):
So, we want to get information about our gnomes in JSON format. Let's try the easiest way - use the standard parameters of the Gson library by creating an instance of the class of the same name and calling the method
Actually, an instance of the class
Well, you can already work with this, however, if you think about it, there are a few comments:
If we take into account all the comments, then we want to see information about the gnome in this format:
Gson provides us with some useful annotations for setting up serialization. Let's see if they can help us.
With the first problem - yes, we can change the output name of the property by adding the SerializedName annotation to the acc. class field. That is, by doing so:
We get the property with the name "age" instead of "dwarfAge".
Already not bad, let's move on. It is necessary to exclude the field
Another way is to use the Expose annotation . It works only in tandem with the GsonBuilder.excludeFieldsWithoutExposeAnnotation () method , which excludes all fields from processing that are nothaving Expose annotations. But it turns out that to exclude one field, we need to add annotations to all other fields. Not too convenient, right?
A more flexible way is to create your own class that serializes objects of a certain type. To do this, you must implement the JsonSerializer interface where T is the type of processed objects. Consider a single
It takes three parameters:
The return data type of the method is

The figure below shows an example of a combination of types:

So, enough theory, let's finally serialize!
First, how many data types do we have that need custom processing?
First, of course, is the class itself that describes the gnome -
Secondly, the beard and mustache class -
It can also be attributed here,
Accordingly, we need two implementations
In order for Gson to use our serializers when processing gnomes, you need to register it using the
We realize to begin with the processing of beards and mustaches. Below is the full code, which we will analyze in more detail below:
Everything is pretty simple. Since we reduce information about the beard and mustache to one line, the result of the serialize () method should be an object
For example, if a gnome has neither a beard nor a mustache, one can question his attitude to the gnome genus:
Otherwise, using a rather trivial algorithm, we get from the source data a string of the type we need, and also create an instance based on it
Now we implement the processing of the gnome as a whole (we also omit the checks):
Let's parse this code in parts. Since as a result we should get a JSON object, we create a variable of the corresponding type:
Then, using the addProperty () method, we enter data of primitive types into our object (without creating an intermediate
Next we need to add data about the beard. To do this, we use the
It remains only to add information about the weapons of the gnome. Since we do not have any symbolic keys for weapons, we create an instance to store them
Now you need to fill the created array with elements. The class
Finally, we use the fruit of our labor for its intended purpose:
We look at what we got:
The only thing I would like to change is that all the gnomes we have are elements of an array that is stored in the “dwarves” property. This is somehow solid, and redundant - we know that we are talking about gnomes, right? Let each gnome be a separate property of the JSON object, where the key is the name of the gnome. For instance:
Most likely, you yourself can imagine what needs to be done to bring this final touch to life. But just in case:
And we got the desired data format for the company of the gnomes:
You can safely go for the blue mountains, for the white fog!
Returning from a JSON adventure, the gnome squad naturally wants to transform itself into cozy Java objects. For the inverse transform, i.e. deserialization, Gson has a method
This is natural, because for the conversion we used our own algorithms, and the default Gson does not know how to process our format. Therefore, in exactly the same way, we must create special deserializers and tell the library that they need to be used to process information about gnomes. As you may have already guessed, to create them you need to implement the JsonDeserializer interface and its only method is deserialize () .
Accepted parameters:
The returned data type is parameterized.
Let's get started!
Let's start small. Restore data about the beard and mustache. Full deserializer code:
Yes, in a good way, it would be worth checking the input data more carefully, but we will take it for granted that it is correct so as not to complicate the code of the examples.
The most important line in this method:
The getAsString () method converts the contents
We are sure that we get the input
It is time to restore the data on the gnome. We do it like this:
Again, some checks are omitted for brevity. Let's analyze in parts.
We know that information about the gnome is presented in the form
We retrieve the age, using first the method
Restore beard data to an object of type
We get the value of the "weapons" property right away in the form of a Json array. One could first get
It remains to walk through the array, restoring weapons data:
For each element, we check whether it belongs to the type
Notice something is missing? And not just “something,” but the name of the gnome! To complete the recovery of gnome information by adding this important detail, let's move on to the last deserializer.
Finally, add a handler for the entire gnome squad:
As with gnome processing, we cast the input to type
The name that is left blank is contained in the element key. We write it to our object and - voila - information about the gnome is completely restored!
It remains to register our freshly baked deserializers, and you can begin the journey "There and Back." Registration is exactly the same as registering serializers:
To test, we first convert the company of the gnomes to a Json string, then back, and for clarity, output the result as a Json object obtained using the standard Gson mechanism. You can make sure that no one is forgotten and nothing is forgotten, all the gnomes returned safe and sound!
So, we have examined the journey "There" (from Java to JSON) and "Back" (from JSON to Java). Each time in our serializers and deserializers, we worked with an intermediate layer of objects of the kind

And although it is quite convenient, it leads to overhead. Gson gives us the opportunity to sacrifice convenience for performance by eliminating the middle layer. You can do this by using not a pair of JsonSerializer + JsonDeserializer for custom conversion, but an implementation of the TypeAdapter class , which is just designed to convert in both directions. What interests us most are the two abstract methods of this class -

Remember, we threw the gnome’s weapon to its own devices by default? Let's fix this injustice. Combine the name and origin of the weapon in a line like “Slasher from Gondolin”. And in order not to be trifled, we will create
Теперь мы, по старой схеме, должны уведомить Gson о новом обработчике для списка оружия, вызвав метод . С его помощью мы можем получить нужный нам параметризованный тип следующим образом:
In fact, we specially inherit a parameterized class by
It remains only to change the code for serialization and deserialization of the gnome, passing control on the processing of weapons to the context indicating the type of value being processed:
That's all, the adapter is connected. Oh yes, it remains to realize it. As usual, under the spoiler is the complete code, which we will analyze further in parts.
So, the method is responsible for the conversion there
We see in the method parameters an instance of the JsonWriter class and our list of weapons.
These commands, in fact, are responsible for arranging square brackets (as, in fact, arrays in JSON are designated). Since we want to get an array at the output, we start it at the beginning of the method and finish it at the end. Everything is pretty simple here. Similarly used methods
Further, in the case of conventional weapons, we simply write a value of a primitive type (string) to the array by calling the method
А для уникального оружия создаем объект и записываем в него две пары ключ-значения, вызывая поочередно методы
Вот и всё, массив с оружием записан.
Мы довольно лихо преобразовали наше оружие в JSON-массив со смешанным типом данных, не так ли? И теперь настало время преобразовать его обратно. И тут нас ждет небольшая проблема. Итак, метод
Класс JsonReader занимается извлечением данных из Json, и тоже в формате потока. Поэтому мы должны последовательно перебрать все «узлы», соответствующим образом их обработав.
По аналогии с записью, объекты и массивы обрабатываются методами
Cвойства объектов мы перебираем методом
Но все это хорошо, если у нас есть строгий формат данных, с определенной последовательностью элементов. Тогда мы знаем, когда открывать массив, когда объект, и так далее. В нашем же случае мы имеем дело со смешанным типом данных массива, где Json-объекты и строки могут идти в любом порядке. К счастью, у
Таким образом, общий вид метода
Мы знаем, что арсенал гнома представлен массивом, в котором содержатся объекты (для уникальных экземпляров) и строки (для обычных). Следовательно, обрабатывая каждый элемент массива, мы проверяем тип начального узла этого элемента. Для обработки строк и объектов у нас созданы методы, которые мы и вызываем. Прочие типы просто пропускаем методом
Метод создания обычного оружия крайне прост:
Просто получаем строку, в которой содержится тип оружия, методом
С уникальным оружием — несколько сложнее:
Мы заходим в объект и перебираем все его свойства с помощью метода
Таким образом, десериализация арсенала гнома с помощью TypeAdapter готова.
На всякий случай — проверим, всё ли в порядке.
Вот и подошло к концу путешествие из Java в JSON и обратно. На этом позвольте откланяться, дорогой читатель. Надеюсь, вам было интересно.
Напомню несколько ссылок, которые могут пригодиться:
И жили они долго и счастливо.
Конец.
After working with Gson, I decided to write this tutorial, which illustrates the principles of working with the library using an example. The post turned out to be relatively long, but I don’t want to split it because of the logical coherence of the story.
First you need to select some subject area. Say, I don’t know, for some reason, the thought of a detachment of gnomes comes to mind. Actually, why not?

Yes, all the code involved in the article can be found on GitHub: https://github.com/treble-snake/gson.dwarves
Images, except for the class diagram, are borrowed from the Gson series of articles at http: //www.javacreed .com .
Introduction
About the Dwarves
So, with the "squad" it is clear - this is a certain set of gnomes. But what about the dwarves themselves? The most important detail characterizing a gnome is, of course, a beard. You can describe the features and classifications of gnome beards for a long time, but for simplicity we define three parameters: whether the gnome has a mustache, if there is a beard, and what color they are. Further, the name and age - where without them. Add something else personal, say the dwarf ate lunch. And finally, weapons. A gnome can have a lot of weapons, and it can be simple, or it can be unique, having its own name and origin.
The result is something like this:

Domain Class Descriptions
For brevity, I will give all the classes in one listing:
public class DwarvesBand
{
List dwarves = new LinkedList<>();
// getters & setters
}
public class Dwarf
{
private String name;
private FacialHair facialHair;
private List weapons = new LinkedList<>();
private String lunch;
private int dwarfAge;
public Dwarf()
{
}
public Dwarf(String name, int dwarfAge)
{
this.name = name;
this.dwarfAge = dwarfAge;
}
// getters & setters
}
/**
* Описание растительности на лице
*/
public class FacialHair
{
private boolean haveBeard;
private boolean haveMustache;
private String color;
public FacialHair(boolean haveBeard, boolean haveMustache, String color)
{
this.haveBeard = haveBeard;
this.haveMustache = haveMustache;
this.color = color;
}
// getters & setters
}
public class Weapon
{
private String type;
public Weapon()
{
// do nothing
}
public Weapon(String type)
{
this.type = type;
}
// getters & setters
}
public class UniqueWeapon extends Weapon
{
private String name;
private String origin;
public UniqueWeapon()
{
super();
}
public UniqueWeapon(String type, String name, String origin)
{
super(type);
this.name = name;
this.origin = origin;
}
// getters & setters
}
We initialize our gnome company by adding three participants ( all characters are fictitious, and coincidences are random ):
public class BandUtil
{
public static DwarvesBand createBand()
{
DwarvesBand company = new DwarvesBand();
Dwarf tmpDwarf;
tmpDwarf = new Dwarf("Orin", 90);
tmpDwarf.setLunch("Ale with chicken");
tmpDwarf.setFacialHair(new FacialHair(true, true, "black"));
tmpDwarf.addWeapon(new UniqueWeapon("sword", "Slasher", "Gondolin"));
tmpDwarf.addWeapon(new UniqueWeapon("shield", "Oaken Shield", "Moria"));
tmpDwarf.addWeapon(new Weapon("dagger"));
company.addDwarf(tmpDwarf);
tmpDwarf = new Dwarf("Kori", 60);
// no lunch :(
tmpDwarf.setFacialHair(new FacialHair(false, true, "red"));
tmpDwarf.addWeapon(new Weapon("mace"));
tmpDwarf.addWeapon(new Weapon("bow"));
company.addDwarf(tmpDwarf);
tmpDwarf = new Dwarf("Billy Bob", 45);
tmpDwarf.setLunch("Ale with chicken and potatoes, tea with some cakes");
tmpDwarf.setFacialHair(new FacialHair(false, false, ""));
company.addDwarf(tmpDwarf);
return company;
}
}
There
Default
So, we want to get information about our gnomes in JSON format. Let's try the easiest way - use the standard parameters of the Gson library by creating an instance of the class of the same name and calling the method
toJson()
.DwarvesBand band = BandUtil.createBand();
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.create();
String json = gson.toJson(band);
Actually, an instance of the class
Gson
could also be created via the operator new
, but then the output JSON would not have been formatted, which is good for exchanging data between applications (it is faster to form, less weight), but not great for human perception. Therefore, we used a special GsonBuilder , calling the method setPrettyPrinting()
that allowed us to see the output JSON in the following form:JSON after serialization by default
{
"dwarves": [
{
"name": "Orin",
"facialHair": {
"haveBeard": true,
"haveMustache": true,
"color": "black"
},
"weapons": [
{
"name": "Slasher",
"origin": "Gondolin",
"type": "sword"
},
{
"name": "Oaken Shield",
"origin": "Moria",
"type": "shield"
},
{
"type": "dagger"
}
],
"lunch": "Ale with chicken",
"dwarfAge": 90
},
{
"name": "Kori",
"facialHair": {
"haveBeard": false,
"haveMustache": true,
"color": "red"
},
"weapons": [
{
"type": "mace"
},
{
"type": "bow"
}
],
"dwarfAge": 60
},
{
"name": "Billy Bob",
"facialHair": {
"haveBeard": false,
"haveMustache": false,
"color": ""
},
"weapons": [],
"lunch": "Ale with chicken and potatoes, tea with some cakes",
"dwarfAge": 45
}
]
}
Well, you can already work with this, however, if you think about it, there are a few comments:
- What stupid property name is "dwarfAge"? And so it is clear that we are talking about a gnome. It’s just that age would have looked a lot better.
- Perhaps the information about lunch is not so important. You can do without her.
- The description of the beard is somehow dry, this should not be allowed. It must be described with a complete sentence, that is, with a line, for example: “Red beard and mustache” or “Black mustache”.
- Why do we need to get an object with a single “type” property for conventional weapons? It will cost just a string.
If we take into account all the comments, then we want to see information about the gnome in this format:
{
"name": "Orin",
"facialHair": "Black beard and mustache",
"weapons": [
{
"name": "Slasher",
"origin": "Gondolin",
"type": "sword"
},
...
,
"dagger"
],
"age": 90
}
Annotations
Gson provides us with some useful annotations for setting up serialization. Let's see if they can help us.
With the first problem - yes, we can change the output name of the property by adding the SerializedName annotation to the acc. class field. That is, by doing so:
@SerializedName("age")
private int dwarfAge;
We get the property with the name "age" instead of "dwarfAge".
Already not bad, let's move on. It is necessary to exclude the field
lunch
. First, you can do this by adding the transient keyword to it , in which case the field will not be taken into account during serialization. But not the fact that this is the right way. The fact that we do not need information about lunch here does not mean that it is not needed with any other serialization. Another way is to use the Expose annotation . It works only in tandem with the GsonBuilder.excludeFieldsWithoutExposeAnnotation () method , which excludes all fields from processing that are nothaving Expose annotations. But it turns out that to exclude one field, we need to add annotations to all other fields. Not too convenient, right?
Custom serializer
A more flexible way is to create your own class that serializes objects of a certain type. To do this, you must implement the JsonSerializer interface
serialize()
interface method :JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context)
It takes three parameters:
T src
- in fact, a serializable object;Type typeOfSrc
- type of serializable object;JsonSerializationContext context
- serialization context; the interface isJsonSerializationContext
also functional and contains 1 method, tooserialize()
; it should be used to process non-primitive data included in a serializable object (and we will do it a little lower); the context inherits all the settings (including registered serializers, etc.) of the original Gson object.
The return data type of the method is
JsonElement
. This is an abstract class having 4 implementations, shown in the figure below:
JsonNull
- in fact, representation fornull
JsonPrimitive
- representation of primitive types like strings, numbers, etc.JsonArray
- a lot of type objectsJsonElement
; can be considered as ; elements can be any of the implementations , and mixed types are supported;List
JsonElement
JsonObject
- a lot of key-value pairs, where the key is a string, and the value is again an object of typeJsonElement
; similar to structure .Map
The figure below shows an example of a combination of types:

Time to serialize the gnomes
So, enough theory, let's finally serialize!
First, how many data types do we have that need custom processing?
First, of course, is the class itself that describes the gnome -
Dwarf
. Secondly, the beard and mustache class -
FacialHair
. It can also be attributed here,
Weapon
and especially UniqueWeapon
, but let us leave it in the custody of processing by default. Accordingly, we need two implementations
JsonSerializer
. They look quite similar:public class DwarfSerializer implements JsonSerializer
{
@Override
public JsonElement serialize(Dwarf src, Type typeOfSrc, JsonSerializationContext context)
{
// сериализуем гнома!
return null;
}
}
public class FacialHairSerializer implements JsonSerializer
{
@Override
public JsonElement serialize(FacialHair src, Type typeOfSrc, JsonSerializationContext context)
{
// сериализуем усы и бороду!
return null;
}
}
In order for Gson to use our serializers when processing gnomes, you need to register it using the
registerTypeAdapter()
class method GsonBuilder
as follows:Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(Dwarf.class, new DwarfSerializer())
.registerTypeAdapter(FacialHair.class, new FacialHairSerializer())
.create();
Beard and Mustache
We realize to begin with the processing of beards and mustaches. Below is the full code, which we will analyze in more detail below:
public class FacialHairSerializer implements JsonSerializer
{
@Override
public JsonElement serialize(FacialHair src, Type typeOfSrc, JsonSerializationContext context)
{
if (!src.isHaveBeard() && !src.isHaveMustache())
return new JsonPrimitive("is he really a dwarf?");
List list = new LinkedList();
if (src.isHaveBeard())
{
list.add("beard");
}
if (src.isHaveMustache())
{
list.add("mustache");
}
return new JsonPrimitive(
new StringBuilder(src.getColor())
.append(" ")
.append(StringUtils.join(list, " and "))
.toString()
);
}
}
Everything is pretty simple. Since we reduce information about the beard and mustache to one line, the result of the serialize () method should be an object
JsonPrimitive
containing the desired line. For example, if a gnome has neither a beard nor a mustache, one can question his attitude to the gnome genus:
if (!src.isHaveBeard() && !src.isHaveMustache())
return new JsonPrimitive("is he really a dwarf?");
Otherwise, using a rather trivial algorithm, we get from the source data a string of the type we need, and also create an instance based on it
JsonPrimitive
. And yes, let’s take it for granted that the input object and hair color are always initialized in order not to complicate the code with checks that are completely unimportant for the educational purposes of the article.Gnome himself
Now we implement the processing of the gnome as a whole (we also omit the checks):
public class DwarfSerializer implements JsonSerializer
{
@Override
public JsonElement serialize(Dwarf src, Type typeOfSrc, JsonSerializationContext context)
{
JsonObject result = new JsonObject();
result.addProperty("name", src.getName());
result.addProperty("age", src.getDwarfAge());
result.add("facialHair", context.serialize(src.getFacialHair()));
JsonArray weapons = new JsonArray();
result.add("weapons", weapons);
for(Weapon weapon : src.getWeapons()) {
weapons.add(
weapon instanceof UniqueWeapon ?
context.serialize(weapon) :
new JsonPrimitive(weapon.getType())
);
}
return result;
}
}
Let's parse this code in parts. Since as a result we should get a JSON object, we create a variable of the corresponding type:
JsonObject result = new JsonObject();
Then, using the addProperty () method, we enter data of primitive types into our object (without creating an intermediate
JsonPrimitive
-object). We pass two parameters to the method: the first is the key, that is, the name of the property of the JSON object, the second is, in fact, the value of this property. Here we set the name of the property “age” instead of “dwarfAge”, and also exclude information about lunch from the result - just without adding it to the resulting object.result.addProperty("name", src.getName());
result.addProperty("age", src.getDwarfAge());
Next we need to add data about the beard. To do this, we use the
serialize()
context method - as mentioned earlier, the context is aware of registered serializers, therefore, FacialHair
our class will apply FacialHairSerializer
. JsonElement
We add the resulting one to our object using the add () method , specifying the desired property name.result.add("facialHair", context.serialize(src.getFacialHair()));
It remains only to add information about the weapons of the gnome. Since we do not have any symbolic keys for weapons, we create an instance to store them
JsonArray
and add it to our object using the same add () method.JsonArray weapons = new JsonArray();
result.add("weapons", weapons);
Now you need to fill the created array with elements. The class
JsonArray
also has an add () method , but it takes only one type parameter JsonElement
, which is logical - the key is not needed in this case. When adding conventional weapons, create JsonPrimitive
on the basis of the string, and serialize the unique using the context. In this case, the standard serialization mechanism will work, because UniqueWeapon
we did not register any handlers for the class .weapons.add(
weapon instanceof UniqueWeapon ?
context.serialize(weapon) :
new JsonPrimitive(weapon.getType())
);
Result
Finally, we use the fruit of our labor for its intended purpose:
DwarvesBand band = BandUtil.createBand();
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(Dwarf.class, new DwarfSerializer())
.registerTypeAdapter(FacialHair.class, new FacialHairSerializer())
.create();
String json = gson.toJson(band);
We look at what we got:
Output json
{
"dwarves": [
{
"name": "Orin",
"age": 90,
"facialHair": "black beard and mustache",
"weapons": [
{
"name": "Slasher",
"origin": "Gondolin",
"type": "sword"
},
{
"name": "Oaken Shield",
"origin": "Moria",
"type": "shield"
},
"dagger"
]
},
{
"name": "Kori",
"age": 60,
"facialHair": "red mustache",
"weapons": [
"mace",
"bow"
]
},
{
"name": "Billy Bob",
"age": 45,
"facialHair": "is he really a dwarf?",
"weapons": []
}
]
}
Finishing touch
The only thing I would like to change is that all the gnomes we have are elements of an array that is stored in the “dwarves” property. This is somehow solid, and redundant - we know that we are talking about gnomes, right? Let each gnome be a separate property of the JSON object, where the key is the name of the gnome. For instance:
{
"Kori": {
"age": 60,
"facialHair": "red mustache",
"weapons": [ ... ]
},
...
}
Most likely, you yourself can imagine what needs to be done to bring this final touch to life. But just in case:
Implementation
1. Add a serializer for the entire gnome company:
2. We remove the name information from the gnome serializer (class
3. Register the squad serializer by adding a
public class DwarvesBandSerializer implements JsonSerializer
{
@Override
public JsonElement serialize(DwarvesBand src, Type typeOfSrc, JsonSerializationContext context)
{
JsonObject result = new JsonObject();
for(Dwarf dwarf : src.getDwarves()) {
result.add(dwarf.getName(), context.serialize(dwarf));
}
return result;
}
}
2. We remove the name information from the gnome serializer (class
DwarfSerializer
) by deleting the line:result.addProperty("name", src.getName());
3. Register the squad serializer by adding a
registerTypeAdapter()
class method call GsonBuilder
:.registerTypeAdapter(DwarvesBand.class, new DwarvesBandSerializer())
And we got the desired data format for the company of the gnomes:
Ta-daa!
{
"Orin": {
"age": 90,
"facialHair": "black beard and mustache",
"weapons": [
{
"name": "Slasher",
"origin": "Gondolin",
"type": "sword"
},
{
"name": "Oaken Shield",
"origin": "Moria",
"type": "shield"
},
"dagger"
]
},
"Kori": {
"age": 60,
"facialHair": "red mustache",
"weapons": [
"mace",
"bow"
]
},
"Billy Bob": {
"age": 45,
"facialHair": "is he really a dwarf?",
"weapons": []
}
}
You can safely go for the blue mountains, for the white fog!
Back
Returning from a JSON adventure, the gnome squad naturally wants to transform itself into cozy Java objects. For the inverse transform, i.e. deserialization, Gson has a method
fromJson()
. It takes two parameters: data in several formats (including String
which we will use) and the type of the result returned. However, if we try to simply create a Gson object and call this method, as shown below, we get an instance of the class DwarvesBand
with an empty list of gnomes:DwarvesBand dwarvesBand = new Gson().fromJson(json, DwarvesBand.class);
This is natural, because for the conversion we used our own algorithms, and the default Gson does not know how to process our format. Therefore, in exactly the same way, we must create special deserializers and tell the library that they need to be used to process information about gnomes. As you may have already guessed, to create them you need to implement the JsonDeserializer interface
T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
Accepted parameters:
JsonElement json
- Json-element from which to restore data;Type typeOfT
- the type of object that should result from;JsonDeserializationContext context
- context of deserialization; by analogy withJsonSerializationContext
, the interfaceJsonDeserializationContext
contains one methoddeserialize()
; this context inherits all the settings of the gson object
The returned data type is parameterized.
Let's get started!
Borroroda!
Let's start small. Restore data about the beard and mustache. Full deserializer code:
public class FacialHairDeserializer implements JsonDeserializer
{
@Override
public FacialHair deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
FacialHair hair = new FacialHair();
String data = json.getAsString();
List parts = Arrays.asList(data.split(" "));
if(parts.contains("beard"))
hair.setHaveBeard(true);
if(parts.contains("mustache"))
hair.setHaveMustache(true);
if(hair.isHaveBeard() || hair.isHaveMustache())
hair.setColor(parts.get(0));
return hair;
}
}
Yes, in a good way, it would be worth checking the input data more carefully, but we will take it for granted that it is correct so as not to complicate the code of the examples.
The most important line in this method:
String data = json.getAsString();
The getAsString () method converts the contents
JsonElement
to a string if applied to a type element JsonPrimitive
containing a valid string, or to JsonArray
containing only one such type element JsonPrimitive
. Otherwise, the method will throw an exception. Similarly, all view methods work getAs{JavaType}()
. We are sure that we get the input
JsonPrimitive
with a string, so we won’t check this (we could use the method isJsonPrimitive()
). Further processing of the obtained data is trivial, we will not linger on it.Gnome
It is time to restore the data on the gnome. We do it like this:
public class DwafDeserializer implements JsonDeserializer
{
@Override
public Dwarf deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
JsonObject jsonObject = json.getAsJsonObject();
Dwarf dwarf = new Dwarf();
dwarf.setDwarfAge(jsonObject.get("age").getAsInt());
dwarf.setFacialHair((FacialHair) context.deserialize(jsonObject.get("facialHair"), FacialHair.class));
JsonArray weapons = jsonObject.getAsJsonArray("weapons");
for(JsonElement weapon : weapons) {
if(weapon.isJsonPrimitive()) {
dwarf.addWeapon(new Weapon(weapon.getAsString()));
} else {
dwarf.addWeapon((UniqueWeapon) context.deserialize(weapon, UniqueWeapon.class));
}
}
return dwarf;
}
}
Again, some checks are omitted for brevity. Let's analyze in parts.
We know that information about the gnome is presented in the form
JsonObject
, therefore, we will convert the input data to this type without checking.JsonObject jsonObject = json.getAsJsonObject();
We retrieve the age, using first the method
get()
that returns us JsonElement
with the value of the specified property “age”, and then the method getAsInt()
, since age is an integer type.dwarf.setDwarfAge(jsonObject.get("age").getAsInt());
Restore beard data to an object of type
FacialHair
using context.deserialize()
. As we recall, the context is aware that a special deserializer must be used to process beard information.dwarf.setFacialHair((FacialHair) context.deserialize(jsonObject.get("facialHair"), FacialHair.class));
We get the value of the "weapons" property right away in the form of a Json array. One could first get
JsonElement
the get (“weapons”) method, then check for belonging to the type of the array by the method isJsonArray()
, and only then convert to the array using the method getAsJsonArray()
. But we believe in our gnomes and the format of their input.JsonArray weapons = jsonObject.getAsJsonArray("weapons");
It remains to walk through the array, restoring weapons data:
for(JsonElement weapon : weapons) {
if(weapon.isJsonPrimitive()) {
dwarf.addWeapon(new Weapon(weapon.getAsString()));
} else {
dwarf.addWeapon((UniqueWeapon) context.deserialize(weapon, UniqueWeapon.class));
}
}
For each element, we check whether it belongs to the type
JsonPrimitive
. We remember that conventional weapons are described by a simple line, which corresponds to this type. In this case, we create an instance of conventional weapons, getting its type by the method getAsString()
. Otherwise, we are dealing with a unique weapon. We processed it using context using standard Gson mechanisms. We do the same thing now, using context.deserialize()
. Notice something is missing? And not just “something,” but the name of the gnome! To complete the recovery of gnome information by adding this important detail, let's move on to the last deserializer.
Detachment
Finally, add a handler for the entire gnome squad:
public class DwarvesBandDeserializer implements JsonDeserializer
{
@Override
public DwarvesBand deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
DwarvesBand result = new DwarvesBand();
JsonObject jsonObject = json.getAsJsonObject();
for(Map.Entry entry : jsonObject.entrySet()) {
Dwarf dwarf = context.deserialize(entry.getValue(), Dwarf.class);
dwarf.setName(entry.getKey());
result.addDwarf(dwarf);
}
return result;
}
}
As with gnome processing, we cast the input to type
JsonObject
. Remember, previously mentioned, what JsonObject
can be perceived as ? By analogy with , there is a method that returns many key-value elements. It is with his help that we will go through a cycle through all the records about the gnomes.
The value of an element is all information about the gnome except the name. We use the context to deserialize this information and get an instance of the Dwarf class.Map
Map
JsonObject
entrySet()
Dwarf dwarf = context.deserialize(entry.getValue(), Dwarf.class);
The name that is left blank is contained in the element key. We write it to our object and - voila - information about the gnome is completely restored!
dwarf.setName(entry.getKey());
Home sweet home
It remains to register our freshly baked deserializers, and you can begin the journey "There and Back." Registration is exactly the same as registering serializers:
Gson gson = new GsonBuilder()
.registerTypeAdapter(DwarvesBand.class, new DwarvesBandDeserializer())
.registerTypeAdapter(FacialHair.class, new FacialHairDeserializer())
.registerTypeAdapter(Dwarf.class, new DwafDeserializer())
.create();
To test, we first convert the company of the gnomes to a Json string, then back, and for clarity, output the result as a Json object obtained using the standard Gson mechanism. You can make sure that no one is forgotten and nothing is forgotten, all the gnomes returned safe and sound!
Security Code
DwarvesBand company = BandUtil.createBand();
Gson gson;
gson = new GsonBuilder()
.registerTypeAdapter(Dwarf.class, new DwarfSerializer())
.registerTypeAdapter(FacialHair.class, new FacialHairSerializer())
.registerTypeAdapter(DwarvesBand.class, new DwarvesBandSerializer())
.create();
String json = gson.toJson(company);
gson = new GsonBuilder()
.registerTypeAdapter(DwarvesBand.class, new DwarvesBandDeserializer())
.registerTypeAdapter(FacialHair.class, new FacialHairDeserializer())
.registerTypeAdapter(Dwarf.class, new DwafDeserializer())
.create();
DwarvesBand bandIsBack = gson.fromJson(json, DwarvesBand.class);
gson = new GsonBuilder()
.setPrettyPrinting()
.create();
System.out.println(gson.toJson(bandIsBack));
Result
{
"dwarves": [
{
"name": "Orin",
"facialHair": {
"haveBeard": true,
"haveMustache": true,
"color": "black"
},
"weapons": [
{
"name": "Slasher",
"origin": "Gondolin",
"type": "sword"
},
{
"name": "Oaken Shield",
"origin": "Moria",
"type": "shield"
},
{
"type": "dagger"
}
],
"dwarfAge": 90
},
{
"name": "Kori",
"facialHair": {
"haveBeard": false,
"haveMustache": true,
"color": "red"
},
"weapons": [
{
"type": "mace"
},
{
"type": "bow"
}
],
"dwarfAge": 60
},
{
"name": "Billy Bob",
"facialHair": {
"haveBeard": false,
"haveMustache": false,
"color": ""
},
"weapons": [],
"dwarfAge": 45
}
]
}
Round trip
So, we have examined the journey "There" (from Java to JSON) and "Back" (from JSON to Java). Each time in our serializers and deserializers, we worked with an intermediate layer of objects of the kind
JsonElement
that Gson kindly provided to us. 
And although it is quite convenient, it leads to overhead. Gson gives us the opportunity to sacrifice convenience for performance by eliminating the middle layer. You can do this by using not a pair of JsonSerializer + JsonDeserializer for custom conversion, but an implementation of the TypeAdapter class
write()
and read()
. They are responsible for custom transformations: write()
- for serialization, and read()
- for deserialization. 
Remember, we threw the gnome’s weapon to its own devices by default? Let's fix this injustice. Combine the name and origin of the weapon in a line like “Slasher from Gondolin”. And in order not to be trifled, we will create
TypeAdapter
for the entire list of weapons, and not just for unique copies. Our class will look like this:public class WeaponsTypeAdapter extends TypeAdapter>
{
@Override
public void write(JsonWriter out, List value) throws IOException
{
// Java → JSON
}
@Override
public List read(JsonReader in) throws IOException
{
// JSON → Java
return null;
}
}
Теперь мы, по старой схеме, должны уведомить Gson о новом обработчике для списка оружия, вызвав метод
.registerTypeAdapter()
. Однако, есть тут загвоздка. Первый параметр метода — это тип данных, для которого регистрируется обработчик, а оружие гнома у нас реализовано обычным списком: List
. И мы явно не хотим, чтобы все другие списки обрабатывались нашим TypeAdapter'ом. Нужно как-то указать, что он предназначен только для списка оружия, передав параметризованный тип. Для этого в Gson используется специальный хитрый класс — TypeTokenType weaponsListType = new TypeToken>(){}.getType();
In fact, we specially inherit a parameterized class by
TypeToken
an anonymous class, so that we can then getGenericSuperclass()
get the type parameterizing parent of the method . In our case, the type parameterizing the parent is ours . Somewhat confusing, but in a different way, alas, nothing. You can read more about obtaining parameters of Generic classes, for example, in this article .
Well and further - as usual:List
Type weaponsListType = new TypeToken>(){}.getType();
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(Dwarf.class, new DwarfSerializerWithTypeAdapter())
.registerTypeAdapter(FacialHair.class, new FacialHairSerializer())
.registerTypeAdapter(DwarvesBand.class, new DwarvesBandSerializer())
.registerTypeAdapter(weaponsListType, new WeaponsTypeAdapter())
.create();
It remains only to change the code for serialization and deserialization of the gnome, passing control on the processing of weapons to the context indicating the type of value being processed:
public class DwarfSerializerWithTypeAdapter implements JsonSerializer
{
public JsonElement serialize(...)
{
...
Type weaponsType = new TypeToken>(){}.getType();
result.add("weapons", context.serialize(src.getWeapons(), weaponsType));
...
}
}
public class DwafDeserializerWithTypeAdapter implements JsonDeserializer
{
public Dwarf deserialize(...)
{
...
Type weaponsType = new TypeToken>(){}.getType();
List weapons = context.deserialize(jsonObject.getAsJsonArray("weapons"), weaponsType);
dwarf.addWeapons(weapons);
...
}
}
That's all, the adapter is connected. Oh yes, it remains to realize it. As usual, under the spoiler is the complete code, which we will analyze further in parts.
Full TypeAdapter code
public class WeaponsTypeAdapter extends TypeAdapter>
{
@Override
public void write(JsonWriter out, List value) throws IOException
{
out.beginArray();
for (Weapon weapon : value)
{
if (weapon instanceof UniqueWeapon)
{
UniqueWeapon uWeapon = (UniqueWeapon) weapon;
out.beginObject();
out.name("name")
.value(uWeapon.getName() + " from " + uWeapon.getOrigin());
out.name("type")
.value(uWeapon.getType());
out.endObject();
}
else
{
out.value(weapon.getType());
}
}
out.endArray();
}
@Override
public List read(JsonReader in) throws IOException
{
List result = new LinkedList<>();
in.beginArray();
while (in.hasNext())
{
switch (in.peek())
{
case STRING:
result.add(createCommonWeapon(in));
break;
case BEGIN_OBJECT:
result.add(createUniqueWeapon(in));
break;
default:
in.skipValue();
break;
}
}
return result;
}
private Weapon createCommonWeapon(JsonReader in) throws IOException
{
return new Weapon(in.nextString());
}
private Weapon createUniqueWeapon(JsonReader in) throws IOException
{
UniqueWeapon weapon = new UniqueWeapon();
in.beginObject();
while (in.hasNext())
{
switch (in.nextName())
{
case "name":
String[] tmp = in.nextString().split(" from ");
weapon.setName(tmp[0]);
if (tmp.length > 1)
weapon.setOrigin(tmp[1]);
break;
case "type":
weapon.setType(in.nextString());
break;
default:
in.skipValue();
break;
}
}
in.endObject();
return weapon;
}
}
And there again
So, the method is responsible for the conversion there
write()
. His code is:public void write(JsonWriter out, List value) throws IOException
{
out.beginArray();
for (Weapon weapon : value)
{
if (weapon instanceof UniqueWeapon)
{
UniqueWeapon uWeapon = (UniqueWeapon) weapon;
out.beginObject();
out.name("name")
.value(uWeapon.getName() + " from " + uWeapon.getOrigin());
out.name("type")
.value(uWeapon.getType());
out.endObject();
}
else
{
out.value(weapon.getType());
}
}
out.endArray();
}
We see in the method parameters an instance of the JsonWriter class and our list of weapons.
JsonWriter
allows you to create output JSON in streaming mode. To begin with - we need an array where we will store data about weapons.out.beginArray();
...
out.endArray();
These commands, in fact, are responsible for arranging square brackets (as, in fact, arrays in JSON are designated). Since we want to get an array at the output, we start it at the beginning of the method and finish it at the end. Everything is pretty simple here. Similarly used methods
value()
:out.value(weapon.getType());
А для уникального оружия создаем объект и записываем в него две пары ключ-значения, вызывая поочередно методы
name()
и value()
.out.name("name")
.value(uWeapon.getName() + " from " + uWeapon.getOrigin());
out.name("type")
.value(uWeapon.getType());
Вот и всё, массив с оружием записан.
И опять Обратно
Мы довольно лихо преобразовали наше оружие в JSON-массив со смешанным типом данных, не так ли? И теперь настало время преобразовать его обратно. И тут нас ждет небольшая проблема. Итак, метод
read()
принимает один параметр:public List read(JsonReader in) throws IOException {...}
Класс JsonReader занимается извлечением данных из Json, и тоже в формате потока. Поэтому мы должны последовательно перебрать все «узлы», соответствующим образом их обработав.
По аналогии с записью, объекты и массивы обрабатываются методами
beginObject() / endObject()
и beginArray() / endArray()
. Cвойства объектов мы перебираем методом
nextName()
, их значения — методом next{Type}()
(например, nextString()
). Элементы массивов также перебираются методом next{Type}()
.Но все это хорошо, если у нас есть строгий формат данных, с определенной последовательностью элементов. Тогда мы знаем, когда открывать массив, когда объект, и так далее. В нашем же случае мы имеем дело со смешанным типом данных массива, где Json-объекты и строки могут идти в любом порядке. К счастью, у
GsonReader
есть еще метод peek()
, который возвращает тип следующего узла, не обрабатывая его.Таким образом, общий вид метода
read()
у нас получится таким:@Override
public List read(JsonReader in) throws IOException
{
List result = new LinkedList<>();
in.beginArray();
while (in.hasNext())
{
switch (in.peek())
{
case STRING:
result.add(createCommonWeapon(in));
break;
case BEGIN_OBJECT:
result.add(createUniqueWeapon(in));
break;
default:
in.skipValue();
break;
}
}
in.endArray();
return result;
}
Мы знаем, что арсенал гнома представлен массивом, в котором содержатся объекты (для уникальных экземпляров) и строки (для обычных). Следовательно, обрабатывая каждый элемент массива, мы проверяем тип начального узла этого элемента. Для обработки строк и объектов у нас созданы методы, которые мы и вызываем. Прочие типы просто пропускаем методом
skipValue()
. Метод создания обычного оружия крайне прост:
private Weapon createCommonWeapon(JsonReader in) throws IOException
{
return new Weapon(in.nextString());
}
Просто получаем строку, в которой содержится тип оружия, методом
nextString()
и создаем на ее основе объект.С уникальным оружием — несколько сложнее:
private Weapon createUniqueWeapon(JsonReader in) throws IOException
{
UniqueWeapon weapon = new UniqueWeapon();
in.beginObject();
while (in.hasNext())
{
switch (in.nextName())
{
case "name":
String[] tmp = in.nextString().split(" from ");
weapon.setName(tmp[0]);
if (tmp.length > 1)
weapon.setOrigin(tmp[1]);
break;
case "type":
weapon.setType(in.nextString());
break;
default:
in.skipValue();
break;
}
}
in.endObject();
return weapon;
}
Мы заходим в объект и перебираем все его свойства с помощью метода
nextName()
. Для свойств с именами «name» и «type» у нас есть алгоритмы обработки — мы создаем на их основе экземпляры обычного и уникального оружия. Остальные свойства (буде таковые найдутся), опять же, пропускаем. Таким образом, десериализация арсенала гнома с помощью TypeAdapter готова.
На всякий случай — проверим, всё ли в порядке.
Код проверки
DwarvesBand company = BandUtil.createBand();
Gson gson;
Type weaponsType = new TypeToken>(){}.getType();
gson = new GsonBuilder()
.registerTypeAdapter(Dwarf.class, new DwarfSerializerWithTypeAdapter())
.registerTypeAdapter(FacialHair.class, new FacialHairSerializer())
.registerTypeAdapter(DwarvesBand.class, new DwarvesBandSerializer())
.registerTypeAdapter(weaponsType, new WeaponsTypeAdapter())
.setPrettyPrinting()
.create();
String json = gson.toJson(company);
System.out.println("Serialized:");
System.out.println(json);
gson = new GsonBuilder()
.registerTypeAdapter(DwarvesBand.class, new DwarvesBandDeserializer())
.registerTypeAdapter(FacialHair.class, new FacialHairDeserializer())
.registerTypeAdapter(Dwarf.class, new DwafDeserializerWithTypeAdapter())
.registerTypeAdapter(weaponsType, new WeaponsTypeAdapter())
.create();
DwarvesBand companyIsBack = gson.fromJson(json, DwarvesBand.class);
gson = new GsonBuilder()
.setPrettyPrinting()
.create();
System.out.println("\n\nDeserialized:");
System.out.println(gson.toJson(companyIsBack));
Результат
Serialized:
{
"Orin": {
"age": 90,
"facialHair": "black beard and mustache",
"weapons": [
{
"name": "Slasher from Gondolin",
"type": "sword"
},
{
"name": "Oaken Shield from Moria",
"type": "shield"
},
"dagger"
]
},
"Kori": {
"age": 60,
"facialHair": "red mustache",
"weapons": [
"mace",
"bow"
]
},
"Billy Bob": {
"age": 45,
"facialHair": "is he really a dwarf?",
"weapons": []
}
}
Deserialized:
{
"dwarves": [
{
"name": "Orin",
"facialHair": {
"haveBeard": true,
"haveMustache": true,
"color": "black"
},
"weapons": [
{
"name": "Slasher",
"origin": "Gondolin",
"type": "sword"
},
{
"name": "Oaken Shield",
"origin": "Moria",
"type": "shield"
},
{
"type": "dagger"
}
],
"dwarfAge": 90
},
{
"name": "Kori",
"facialHair": {
"haveBeard": false,
"haveMustache": true,
"color": "red"
},
"weapons": [
{
"type": "mace"
},
{
"type": "bow"
}
],
"dwarfAge": 60
},
{
"name": "Billy Bob",
"facialHair": {
"haveBeard": false,
"haveMustache": false,
"color": ""
},
"weapons": [],
"dwarfAge": 45
}
]
}
Послесловие
Вот и подошло к концу путешествие из Java в JSON и обратно. На этом позвольте откланяться, дорогой читатель. Надеюсь, вам было интересно.
Напомню несколько ссылок, которые могут пригодиться:
- Проект Gson и документация по нему
- Код примера на GitHub
И жили они долго и счастливо.
Конец.