📜 ⬆️ ⬇️

Gson or "There and Back"

Recently, I had to work with the Google Gson library, designed to convert Java objects to JSON (serialized) text format and reverse conversion (deserialization). Often, when working with Gson, the standard settings of the library are enough, but there are cases (including mine) when it is necessary to customize the conversion processes.

Having worked with Gson, I decided to write this tutorial that illustrates the principles of working with the library on an example. The post turned out to be relatively long, but I do not want to split it up because of the logical coherence of the narration.

First you need to choose any subject area. Say, I do not 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
The images, except for the class diagram, are borrowed from the series of articles about Gson at http://www.javacreed.com .

Introduction


About gnomes

So, with the "detachment" is clear - this is a kind of many dwarves. But what about the gnomes themselves? The most important detail that characterizes a dwarf is, of course, a beard. You can paint for a long time the features and classifications of dwarf beards, but for simplicity, we define three parameters: whether the dwarf has a mustache, has a beard, and what color are they. Further, the name and age - where do without them. Add something more personal, let's say that the dwarf ate for lunch. And finally, a weapon. A gnome can have many weapons, and it can be simple, or it can be unique, with its own name and origin.

The result is something like this:


Class listings describing the subject area
For brevity, I will give all the classes in one listing:
public class DwarvesBand { List<Dwarf> dwarves = new LinkedList<>(); // getters & setters } public class Dwarf { private String name; private FacialHair facialHair; private List<Weapon> 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 the actors are fictional and the 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 - to use the standard parameters of the Gson library, creating an instance of the class with the same name and calling the toJson() method.
 DwarvesBand band = BandUtil.createBand(); Gson gson = new GsonBuilder() .setPrettyPrinting() .create(); String json = gson.toJson(band); 

Actually, an instance of the Gson class Gson also be created through the new operator, but then the output JSON would not be formatted, which is good for data exchange between applications (it forms faster, weighs less), but not healthy for human perception. Therefore, we used a special GsonBuilder by calling the setPrettyPrinting() method, which allowed us to see the output JSON as follows:
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, it is already possible to work with this, however, if you think about it, there are a few remarks:
  1. What a stupid property name is “dwarfAge”? And so it is clear that we are talking about a gnome. Just "age" would look much better.
  2. Perhaps the information about lunch is not so important. You can do without it.
  3. Description of the beard is some kind of dry, this should not be allowed. It should be described with a complete sentence, that is, a line, for example: “Red beard and mustache” or “Black mustache”.
  4. 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, we want to see information about the dwarf 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. 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 corresponding. field class. That is, by doing this:

 @SerializedName("age") private int dwarfAge; 

We will get a property named “age” instead of “dwarfAge”.

Already not bad, we go further. You need to exclude the lunch box. 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 dinner here does not mean that it is not needed during some other serialization.
Another way is to use the Expose annotation. It only works in conjunction with the GsonBuilder.excludeFieldsWithoutExposeAnnotation () method, which excludes all fields that do not have the Expose annotation from processing. But, it turns out, to exclude one field, we need to add annotations to all other fields. Not too comfortable, right?

Your serializer

A more flexible way is to create your own class that serializes objects of a certain type. To do this, you need to implement the JsonSerializer <T> interface, where T is the type of objects being processed. Consider a single interface serialize() method:
 JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) 

It takes three parameters:

The returned data type of the method is JsonElement . This is an abstract class with 4 implementations, shown in the figure below:


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


Time to serialize dwarves

So, enough theory, let's finally serialize!
First, how many data types we have that require custom processing.
Firstly, it is, of course, the class itself, describing the gnome - Dwarf .
Secondly, the beard and mustache class is FacialHair .
Still here it is possible to carry Weapon and especially UniqueWeapon , but we will leave it while on care of processing by default.

Accordingly, we need two implementations of the JsonSerializer . They look quite similar:
 public class DwarfSerializer implements JsonSerializer<Dwarf> { @Override public JsonElement serialize(Dwarf src, Type typeOfSrc, JsonSerializationContext context) { //  ! return null; } } public class FacialHairSerializer implements JsonSerializer<FacialHair> { @Override public JsonElement serialize(FacialHair src, Type typeOfSrc, JsonSerializationContext context) { //    ! return null; } } 

In order for Gson to use our serializers when processing dwarves, you need to register it using the registerTypeAdapter() method of the GsonBuilder class as follows:

 Gson gson = new GsonBuilder() .setPrettyPrinting() .registerTypeAdapter(Dwarf.class, new DwarfSerializer()) .registerTypeAdapter(FacialHair.class, new FacialHairSerializer()) .create(); 


Beard and mustache

We realize to start the treatment of the beard and mustache. Below is the full code, which will be discussed in more detail below:
 public class FacialHairSerializer implements JsonSerializer<FacialHair> { @Override public JsonElement serialize(FacialHair src, Type typeOfSrc, JsonSerializationContext context) { if (!src.isHaveBeard() && !src.isHaveMustache()) return new JsonPrimitive("is he really a dwarf?"); List<String> list = new LinkedList<String>(); 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() ); } } 

It's pretty simple. Since we reduce the information about the beard and mustache to one line, the result of the work of the serialize () method should be a JsonPrimitive object containing the desired string.
For example, if a gnome has neither a beard nor a mustache, one may question its attitude to the gnome family:
 if (!src.isHaveBeard() && !src.isHaveMustache()) return new JsonPrimitive("is he really a dwarf?"); 

Otherwise, using a rather trivial algorithm, we obtain from the source data a string of the type we need, and also create an instance of JsonPrimitive on its basis. And yes, let us take for granted that the input object and hair color are always initialized with us, so as not to complicate the code with checks that are completely unimportant for the purposes of the article.

The dwarf himself

Now we implement the processing of the gnome as a whole (also omit the checks):
 public class DwarfSerializer implements JsonSerializer<Dwarf> { @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 sort this code in parts. As a result, we should get a JSON object, we create a variable of the appropriate type:
 JsonObject result = new JsonObject(); 

Then, using the addProperty () method, we enter the 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 the value of this property. This is where we set the name of the property “age” instead of “dwarfAge”, and also exclude information about lunch from the result — simply by not adding it to the resulting object.
 result.addProperty("name", src.getName()); result.addProperty("age", src.getDwarfAge()); 

Next we need to add beard data. To do this, we use the context's serialize() method — as mentioned earlier, the context is aware of the registered serializers, so our FacialHairSerializer FacialHair apply to the FacialHairSerializer . We add the resulting JsonElement 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 have no symbolic keys for weapons, we create an instance of JsonArray to store them 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 JsonArray class also has a add () method, but it accepts only one parameter of the JsonElement type, which is logical - the key is not needed in this case. When adding a conventional weapon, we create a JsonPrimitive based on a string, and uniquely serialize it using a context. In this case, the standard serialization mechanism will work, because we have not registered any handlers for the UniqueWeapon class.
 weapons.add( weapon instanceof UniqueWeapon ? context.serialize(weapon) : new JsonPrimitive(weapon.getType()) ); 

Result

Finally, we use the fruit of our work 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 that we did:
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 dwarves in our country are elements of an array, which is stored in the “dwarves” property. This is somehow disreputable, and redundant - we know that we are talking about dwarves, right? Let each gnome be a separate property of a JSON object, where the key is the name of the gnome. For example:
 { "Kori": { "age": 60, "facialHair": "red mustache", "weapons": [ ... ] }, ... } 

Most likely, you yourself can already imagine what needs to be done to bring this final touch to life. But just in case:
Implementation
1. Add a serializer for the whole gnome company:
 public class DwarvesBandSerializer implements JsonSerializer<DwarvesBand> { @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. Remove the name information from the gnome serializer ( DwarfSerializer class) by deleting the line:
 result.addProperty("name", src.getName()); 


3. Register the squad serializer by adding a call to the registerTypeAdapter() method of the GsonBuilder class:
 .registerTypeAdapter(DwarvesBand.class, new DwarvesBandSerializer()) 


And we got the desired data format for the gnomes company:
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 mist!

Back


Returning from JSON-adventure, the squad of dwarfs naturally wants to be transformed back into cozy Java objects. For the inverse transform, that is, deserialization, Gson has a fromJson() method. It takes two parameters: data in several formats (including String , which we will use) and the type of the returned result. However, if we try to simply create a Gson object and call this method, as shown below, we will get an instance of the DwarvesBand class with an empty list of gnomes:
 DwarvesBand dwarvesBand = new Gson().fromJson(json, DwarvesBand.class); 

This is natural, because we used our own algorithms for the conversion, and the default Gson does not know how to handle our format. Therefore, in exactly the same way, we have to create special deserializers and indicate to the library that it is necessary to use them to process information about gnomes. As you may have already guessed, to create them, you need to implement the JsonDeserializer <T> interface and its only method, deserialize () .
 T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) 

Accepted Parameters:

The returned data type is parameterized.
Let's get started!

Borrrod!

Let's start small. Restore data on the beard and mustache. Full deserialization code:
 public class FacialHairDeserializer implements JsonDeserializer<FacialHair> { @Override public FacialHair deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { FacialHair hair = new FacialHair(); String data = json.getAsString(); List<String> 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 an amicable way, it would be worth checking the input data more carefully, but we take it for granted that they are correct so as not to complicate the code of the examples.
The most important line in this method is:
 String data = json.getAsString(); 

The getAsString () method converts the contents of a JsonElement to a string if it is applied to an element of type JsonPrimitive containing a valid string, or to a JsonArray containing only one such element of type JsonPrimitive . Otherwise, the method will throw an exception. All methods of the getAs{JavaType}() type getAs{JavaType}() .
We are sure that we get JsonPrimitive with a string at the input, so we will not check it (one could use the isJsonPrimitive() method). Further processing of the data obtained is trivial, we will not linger on it.

Dwarf

It is time to recover the data about the gnome. We do it like this:
 public class DwafDeserializer implements JsonDeserializer<Dwarf> { @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. We analyze in parts.
We know that the information about the gnome is presented in the form of a JsonObject , so we will convert the input data to this type without checking.
 JsonObject jsonObject = json.getAsJsonObject(); 

We JsonElement age using the get() method first, which returns us a JsonElement with the value of the specified property “age”, and then the getAsInt() method, since age has an integer type.
 dwarf.setDwarfAge(jsonObject.get("age").getAsInt()); 

Restore the beard data to an object of type FacialHair using context.deserialize() . As we remember, the context is aware of the need to use a special deserializer to process beard information.
 dwarf.setFacialHair((FacialHair) context.deserialize(jsonObject.get("facialHair"), FacialHair.class)); 

We get the value of the “weapons” property immediately in the form of a Json-array. You could first get JsonElement with the get method (“weapons”), then check for the array type using the isJsonArray() method, and then convert it into an array using the getAsJsonArray() method. But we believe in our gnomes and the format of their input.
 JsonArray weapons = jsonObject.getAsJsonArray("weapons"); 

It remains to go through the array, restoring data on weapons:
 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 if it is of JsonPrimitive type. We remember that conventional weapons are described with a simple string, which corresponds to this type. In this case, create an instance of a conventional weapon, getting its type using the getAsString() method. 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() .

Noticed that something is missing? And not just "something", but the name of the gnome! To complete the recovery of information about the gnome, adding this important detail, we turn to the last deserializer.

Squad

Finally, add a handler for the entire squad of gnomes:
 public class DwarvesBandDeserializer implements JsonDeserializer<DwarvesBand> { @Override public DwarvesBand deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { DwarvesBand result = new DwarvesBand(); JsonObject jsonObject = json.getAsJsonObject(); for(Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) { Dwarf dwarf = context.deserialize(entry.getValue(), Dwarf.class); dwarf.setName(entry.getKey()); result.addDwarf(dwarf); } return result; } } 

As in the processing of the gnome, we JsonObject input data to the JsonObject type. Remember, it was previously mentioned that JsonObject can be perceived as Map<String, JsonElement> ? Similar to Map , JsonObject has an entrySet() method that returns a set of key-value elements. Just with his help, we will cycle through all the records about the gnomes.
The value of the element is all the information about the gnome, except the name. We use context to deserialize this information and get an instance of the Dwarf class.
 Dwarf dwarf = context.deserialize(entry.getValue(), Dwarf.class); 

The remaining name is empty in the item key. We write it in our object and - voila - the information about the gnome is fully restored!
 dwarf.setName(entry.getKey()); 


Home, sweet home

It remains to register our newly baked deserializers, and you can start the journey "There and Back." Registration is absolutely similar to the registration of serializers:
 Gson gson = new GsonBuilder() .registerTypeAdapter(DwarvesBand.class, new DwarvesBandDeserializer()) .registerTypeAdapter(FacialHair.class, new FacialHairDeserializer()) .registerTypeAdapter(Dwarf.class, new DwafDeserializer()) .create(); 


To check, we first transform the company of the dwarves into a Json string, then back, and for clarity, we will display 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 dwarfs have 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 considered the trip "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 type JsonElement , which JsonElement kindly provided us.

And although it is quite convenient, it leads to overhead.Gson gives us the opportunity to sacrifice convenience for the sake of performance, eliminating the intermediate layer. You can do this using a custom JsonSerializer + JsonDeserializer pair for the custom conversion, but the implementation of the TypeAdapter <T> class , which is intended for converting to both sides. Most of all we are interested in two abstract methods of this class - write()and read(). They are responsible for custom transformations: write()- for serialization, and read()- for deserialization.

Remember we abandoned the gnome’s guns to the default processing? Let's fix this injustice. Combine the name and origin of the weapon in a string like "Slasher from Gondolin". And in order not to waste time on trifles, createTypeAdapterfor the entire list of weapons, not just for unique copies. Our class will look like this:
 public class WeaponsTypeAdapter extends TypeAdapter<List<Weapon>> { @Override public void write(JsonWriter out, List<Weapon> value) throws IOException { // Java → JSON } @Override public List<Weapon> read(JsonReader in) throws IOException { // JSON → Java return null; } } 

Now we, according to the old scheme, must notify Gson of the new handler for the list of weapons by calling the method .registerTypeAdapter(). However, there is a snag. The first parameter of the method - is a data type for which the handler is registered, and gnome weapons we sold the usual list: List<Weapon>. And we obviously do not want all other lists to be processed by our TypeAdapter. It is necessary to somehow indicate that it is intended only for the list of weapons, passing the parameterized type. For this, Gson uses a special tricky class - TypeToken <T> . With it, we can get the parameterized type we need as follows:
 Type weaponsListType = new TypeToken<List<Weapon>>(){}.getType(); 

In fact, we specifically inherit the parameterized class by the TypeTokenanonymous class, in order to then getGenericSuperclass()obtain the type of the parameterizing parent by method . In our case, the parameterizing parent type is ours List<Weapon>. A little confusing, but in a different way, alas, no way. In more detail about obtaining the parameters of the Generic classes can be read, for example, in this article .
Well and further - as usual:
 Type weaponsListType = new TypeToken<List<Weapon>>(){}.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 serialization and deserialization code of the gnome, transferring control on the processing of weapons to the context indicating the type of value being processed:
 public class DwarfSerializerWithTypeAdapter implements JsonSerializer<Dwarf> { public JsonElement serialize(...) { ... Type weaponsType = new TypeToken<List<Weapon>>(){}.getType(); result.add("weapons", context.serialize(src.getWeapons(), weaponsType)); ... } } public class DwafDeserializerWithTypeAdapter implements JsonDeserializer<Dwarf> { public Dwarf deserialize(...) { ... Type weaponsType = new TypeToken<List<Weapon>>(){}.getType(); List<Weapon> weapons = context.deserialize(jsonObject.getAsJsonArray("weapons"), weaponsType); dwarf.addWeapons(weapons); ... } } 

That's all, the adapter is connected. Oh yeah, it remains to implement it. As usual, under the spoiler - the full code, which will be further discussed in more detail.
Full TypeAdapter code
 public class WeaponsTypeAdapter extends TypeAdapter<List<Weapon>> { @Override public void write(JsonWriter out, List<Weapon> 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<Weapon> read(JsonReader in) throws IOException { List<Weapon> 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; } } 


Again

So, the method is responsible for the transformation "There" write(). Its code is:
 public void write(JsonWriter out, List<Weapon> 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 parameters of the method an instance of the JsonWriter class and our list of weapons. JsonWriterallows you to create output JSON in streaming mode. First of all, we need an array where we will store weapon data.
 out.beginArray(); ... out.endArray(); 

These commands, in fact, are responsible for arranging square brackets (as, in fact, arrays in JSON are denoted). Since we want to get an array at the output, we start it at the beginning of the method, and at the end we finish it. It's all pretty simple. Similarly, the <codebeginObject () </ code and <codeendObject () </ code methods are used to create objects.
Further, in the case of conventional weapons, we simply write to the array the value of a primitive type (string) by calling the method value():
 out.value(weapon.getType()); 

And for a unique weapon, we create an object and write two key-value pairs into it, calling the name()and methods in turn value().
 out.name("name") .value(uWeapon.getName() + " from " + uWeapon.getOrigin()); out.name("type") .value(uWeapon.getType()); 

That's all, an array of weapons recorded.

And back again

We pretty famously transformed our weapon into a JSON array with a mixed data type, right? And now it is time to convert it back. And here we have a small problem. So, the method read()takes one parameter:
 public List<Weapon> read(JsonReader in) throws IOException {...} 

The JsonReader class is engaged in extracting data from Json, and also in stream format. Therefore, we must iterate through all the "nodes", processing them accordingly.
By analogy with the record, objects and arrays are processed by the beginObject() / endObject()and methods beginArray() / endArray().
We iterate the properties of objects with the method nextName(), their values ​​with the method next{Type}()(for example, nextString()). The elements of the arrays are also sorted by the method next{Type}().
But all this is good, if we have a strict data format, with a certain sequence of elements. Then we know when to open an array, when an object, and so on. In our case, we are dealing with a mixed type of data array, where Json-objects and strings can go in any order. Fortunately,GsonReaderthere is also a method peek()that returns the type of the next node without processing it.
Thus, the general form of the method read()we get is:
 @Override public List<Weapon> read(JsonReader in) throws IOException { List<Weapon> 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; } 

We know that the gnome’s arsenal is represented by an array containing objects (for unique instances) and strings (for regular ones). Therefore, processing each element of the array, we check the type of the initial node of this element. To handle strings and objects, we have created methods that we call. Other types are simply skipped by the method skipValue().

The method of creating conventional weapons is extremely simple:
 private Weapon createCommonWeapon(JsonReader in) throws IOException { return new Weapon(in.nextString()); } 

Just get a string that contains the type of weapon, method, nextString()and create an object on its basis.

With a unique weapon - somewhat more complicated:
 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; } 


We go into the object and iterate through all its properties using the method nextName(). For properties with the names "name" and "type" we have processing algorithms - we create on their basis instances of conventional and unique weapons. The remaining properties (if there are any), again, skip.

Thus, deserialization of the gnome’s arsenal using the TypeAdapter is ready.
Just in case - check if everything is in order.

Security Code
 DwarvesBand company = BandUtil.createBand(); Gson gson; Type weaponsType = new TypeToken<List<Weapon>>(){}.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)); 


Result
 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 } ] } 



Afterword


That's the end of the journey from Java to JSON and back. At this let me take my leave, dear reader. I hope you were interested.
Let me remind you a few links that may come in handy:



And they lived happily ever after.
The end.

Source: https://habr.com/ru/post/228279/


All Articles