Недавно мне пришлось поработать с библиотекой Google Gson, предназначенной для преобразования Java-объектов в текстовый формат JSON (сериализация) и обратного преобразования (десереализация). Часто при работе с Gson хватает стандартных настроек библиотеки, но бывают случаи (в том числе мой), когда необходимо кастомизировать процессы преобразований.
Поработав с Gson, я решил написать этот туториал, который иллюстрирует принципы работы с библиотекой на примере. Пост получился сравнительно длинный, но дробить его не хочется из-за логической связности повествования.
Для начала нужно выбрать какую-нибудь предметную область. Скажем, не знаю, почему-то приходят в голову мысль про отряд гномов. Собственно, почему бы и нет?
Да, весь код, задействованный в статье, можно найти на GitHub: https://github.com/treble-snake/gson.dwarves
Изображения, кроме диаграммы классов, позаимствованы из цикла статей о Gson на сайте http://www.javacreed.com.
Введение
О гномах
Итак, с «отрядом» понятно — это некое множество гномов. Но что насчет самих гномов? Самая важная деталь, характеризующая гнома — это, конечно, борода. Можно долго расписывать особенности и классификации гномьих бород, но для простоты определим три параметра: есть ли у гнома усы, есть ли борода, и какого они цвета. Далее, имя и возраст — куда ж без них. Добавим еще что-нибудь личного, скажем, что гном ел на обед. Ну и, наконец, оружие. Оружия у гнома может быть много, причем оно может быть простое, а может быть уникальное, имеющее собственное имя и происхождение.
В итоге получается примерно так:
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
}
Проинициализируем нашу гномью компанию, добавив трех участников (все действующие лица вымышлены, а совпадения случайны):
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;
}
}
Туда
По умолчанию
Итак, мы хотим получить информацию о наших гномах в формате JSON. Попробуем самый простой способ — использовать стандартные параметры библиотеки Gson, создав экземпляр одноименного класса и вызвав метод toJson()
.
DwarvesBand band = BandUtil.createBand();
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.create();
String json = gson.toJson(band);
Собственно, экземпляр класса Gson
можно было создать и через оператор new
, но тогда выходной JSON был бы не отформатирован, что хорошо для обмена данными между приложениями (быстрее формируется, меньше весит), но не здорово для человеческого восприятия. Поэтому мы использовали специальный GsonBuilder, вызывав метод setPrettyPrinting()
, который позволил лицезреть выходной JSON в следующем виде:
{
"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
}
]
}
Что ж, с этим уже можно работать, однако, если подумать, то есть несколько замечаний:
- Что за дурацкое название свойства — «dwarfAge»? И так понятно, что речь идет о гноме. Просто «age» смотрелось бы куда лучше.
- Пожалуй, информация про обед не так уж и важна. Можно обойтись без нее.
- Описание бороды какое-то сухое, такого допускать нельзя. Описывать ее нужно законченным предложением, то есть строкой, например: «Red beard and mustache» или «Black mustache».
- Зачем нам заводить объект с единственным свойством «type» для обычного оружия? Обойдется просто строкой.
Если учесть все замечания, то мы ходим видеть информацию о гноме в таком формате:
{
"name": "Orin",
"facialHair": "Black beard and mustache",
"weapons": [
{
"name": "Slasher",
"origin": "Gondolin",
"type": "sword"
},
...
,
"dagger"
],
"age": 90
}
Аннотации
Gson предоставляет нам несколько полезных аннотаций для настройки сериализации. Посмотрим, смогут ли они помочь нам.
С первой проблемой — да, изменить выходное имя свойства мы можем, добавив аннотацию SerializedName к соотв. полю класса. То есть, сделав так:
@SerializedName("age")
private int dwarfAge;
Мы получим на выходе свойство с именем «age» вместо «dwarfAge».
Уже неплохо, идем дальше. Нужно исключить поле lunch
. Во-первых, сделать это можно, добавив к нему ключевое слово transient, в таком случае поле не будет учитываться при сериализации. Но не факт, что это правильный путь. То, что информация про обед не нужна нам здесь, не значит, что она не нужна при какой-то иной сериализации.
Другой путь — использование аннотации Expose. Она работает только в паре с методом GsonBuilder.excludeFieldsWithoutExposeAnnotation() , который исключает из обработки все поля, не имеющие аннотации Expose. Но, выходит, чтобы исключить одно поле, нам нужно добавить аннотации ко всем остальным полям. Не слишком удобно, верно?
Свой сериализатор
Более гибкий способ — создать свой класс, производящий сериализацию объектов определенного типа. Для этого необходимо реализовать интерфейс JsonSerializer<T>, где T — тип обрабатываемых объектов. Рассмотрим единственный метод serialize()
интерфейса:
JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context)
Он принимает три параметра:
T src
— собственно, сериализуемый объект;Type typeOfSrc
— тип сериализуемого объекта;JsonSerializationContext context
— контекст сериализации; интерфейсJsonSerializationContext
также является функциональным и содержит 1 метод, тожеserialize()
; его стоит использовать для обработки непримитивных данных, входящих в сериализуемый объект (и мы это сделаем чуть ниже); контекст наследует все настройки (в т.ч. зарегистрированные сериализаторы и т.п.) исходного Gson-объекта.
Возвращаемый тип данных метода — JsonElement
. Это абстрактный класс, имеющий 4 реализации, изображенные на рисунке ниже:
JsonNull
— собственно, представление дляnull
JsonPrimitive
— представление примитивных типов вроде строк, чисел и т.д.JsonArray
— множество объектов типаJsonElement
; можно рассматривать какList<JsonElement>
; элементы могут быть любой из реализацийJsonElement
, причем поддерживаются смешанные типы;JsonObject
— множество пар ключ-значение, где ключ — это строка, а значение — опять же объект типаJsonElement
; аналогично структуреMap<String, JsonElement>
.
На рисунке ниже изображен пример сочетания типов:
Время сериализовать гномов
Итак, довольно теории, давайте же наконец сериализовать!
Сперва сколько у нас типов данных, требующих кастомной обработки.
Во-первых, это, конечно, сам класс, описывающий гнома — Dwarf
.
Во-вторых, класс бороды и усов — FacialHair
.
Еще сюда можно отнести Weapon
и особенно UniqueWeapon
, но оставим его пока на попечение обработки по умолчанию.
Соответственно, нам нужны две реализации JsonSerializer
. Выглядят они вполне аналогично:
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;
}
}
Чтобы при обработке гномов Gson использовал наши сериализаторы, нужено зарегистрировать его с помощью метода registerTypeAdapter()
класса GsonBuilder
следующим образом:
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(Dwarf.class, new DwarfSerializer())
.registerTypeAdapter(FacialHair.class, new FacialHairSerializer())
.create();
Борода и усы
Реализуем для начала обработку бороды и усов. Ниже приведен полный код, который далее разберем подробней:
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()
);
}
}
Все довольно просто. Так как информацию о бороде и усах мы сводим к одной строке, то результатом работы метода serialize() должен являться объект JsonPrimitive
, содержащий нужную строку.
Например, если у гнома нет ни бороды, ни усов, можно поставить под сомнение его отношение к гномьему роду:
if (!src.isHaveBeard() && !src.isHaveMustache())
return new JsonPrimitive("is he really a dwarf?");
В ином случае, используя довольно тривиальный алгоритм, получим из исходных данных строку нужного нам вида, и также создадим на её основе экземпляр JsonPrimitive
. И да, примем за данность, что входной объект и цвет волос у нас всегда проинициализирванны, чтобы не усложнять код проверками, совершенно неважными для обучающих целей статьи.
Сам гном
Теперь реализуем обработку гнома целиком (также опустим проверки):
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;
}
}
Разберем этот код по частям. Так как в результате мы должны получит JSON-объект, то создаем переменную соответствующего типа:
JsonObject result = new JsonObject();
Затем с помощью метода addProperty() заносим в наш объект данные примитивных типов (не создавая при этом промежуточный JsonPrimitive
-объект). Передаем в метод два параметра: первый — ключ, то есть название свойства JSON-объекта, второй — собственно, значение этого свойства. Здесь-то мы и задаем имя свойства «age» вместо «dwarfAge», а также исключаем из результата информацию про обед — просто не добавляя её в результирующий объект.
result.addProperty("name", src.getName());
result.addProperty("age", src.getDwarfAge());
Далее нам нужно добавить данные о бороде. Для этого мы используем метод serialize()
контекста — как говорилось ранее, контекст осведомлен о зарегистрированных сериализаторах, поэтому для класса FacialHair
применит наш FacialHairSerializer
. Получившийся JsonElement
мы добавляем к нашему объекту методом add(), указав нужное имя свойства.
result.add("facialHair", context.serialize(src.getFacialHair()));
Осталось только добавить информацию об оружии гнома. Так как никаких символьных ключей для единиц оружия у нас не предусмотрено, то для их хранения создаем экземпляр JsonArray
и добавляем его в наш объект с помощью того же метода add().
JsonArray weapons = new JsonArray();
result.add("weapons", weapons);
Теперь нужно наполнить созданный массив элементами. У класса JsonArray
тоже есть метод add(), но он принимает только один параметр типа JsonElement
, что и логично — ключ в данном случае не нужен. При добавлении обычного оружия создаем JsonPrimitive
на основе строки, а уникальное сериализуем с помощью контекста. В данном случае сработает стандартный механизм сериализации, потому что никаких обработчиков для класса UniqueWeapon
мы не регистрировали.
weapons.add(
weapon instanceof UniqueWeapon ?
context.serialize(weapon) :
new JsonPrimitive(weapon.getType())
);
Результат
Наконец, используем плод нашего труда по прямому назначению:
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);
Смотрим, что у нас получилось:
{
"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": []
}
]
}
Последний штрих
Единственное, что хотелось бы изменить — все гномы у нас являются элементами массива, который хранится в свойстве «dwarves». Это как-то несолидно, да и избыточно — мы же знаем, что речь идет о гномах, так? Пусть каждый гном будет отдельным свойством JSON-объекта, где ключ — имя гнома. Например:
{
"Kori": {
"age": 60,
"facialHair": "red mustache",
"weapons": [ ... ]
},
...
}
Скорее всего, вы уже и сами можете представить, что нужно сделать, чтобы воплотить этот финальный штрих в жизнь. Но на всякий случай:
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. Убираем из сериализатора гнома (класс DwarfSerializer
) информацию об имени, удалив строку:
result.addProperty("name", src.getName());
3. Регистрируем сериализатор отряда, добавив вызов метода registerTypeAdapter()
класса GsonBuilder
:
.registerTypeAdapter(DwarvesBand.class, new DwarvesBandSerializer())
И мы получили желаемый формат данных о компании гномов:
{
"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": []
}
}
Можно смело отправляться за синие горы, за белый туман!
Обратно
Вернувшись из JSON-приключения, отряд гномов, естественно, хочет преобразоваться обратно в уютные Java-объекты. Для обратного преобразования, то есть десериализации, у Gson есть метод fromJson()
. Он принимает два параметра: данные в нескольких форматах (в т.ч. String
, который мы и будем использовать) и тип возвращаемого результата. Однако, если мы попытаемся просто создать объект Gson и вызвать этот метод, как показано ниже, то получим экземпляр класса DwarvesBand
с пустым списком гномов:
DwarvesBand dwarvesBand = new Gson().fromJson(json, DwarvesBand.class);
Это естественно, ведь для преобразования мы использовали собственные алгоритмы, и настроенный по умолчанию Gson не знает, как обрабатывать наш формат. Поэтому, абсолютно аналогичным образом, мы должны создать специальные десериализаторы и указать библиотеке, что для обработки информации о гномах нужно использовать именно их. Как вы уже, возможно, догадались, для их создания нужно реализовать интерфейс JsonDeserializer<T> и его единственный метод deserialize().
T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
Принимаемые параметы:
JsonElement json
— Json-элемент, из которого нужно восстановить данные;Type typeOfT
— тип объекта, который должен получиться в результате;JsonDeserializationContext context
— контекст десериализации; по аналогии сJsonSerializationContext
, интерфейсJsonDeserializationContext
содержит один методdeserialize()
; этот контекст наследует все настройки Gson-объекта
Возвращаемый тип данных — параметризуется.
Приступим!
Борррода!
Начнем с малого. Восстановим данные о бороде и усах. Полный код десериализатора:
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;
}
}
Да, по-хорошему, стоило бы проверять входные данные более тщательно, но примем за данность, что они корректны, дабы не усложнять код примеров.
Самое важная строка в этом методе:
String data = json.getAsString();
Метод getAsString() преобразует содержимое JsonElement
в строку, если применяется к элементу типа JsonPrimitive
, содержащему валидную строку, или к JsonArray
, содержащему только один такой элемент типа JsonPrimitive
. В ином случае метод выбросит исключение. Аналогично работают все методы вида getAs{JavaType}()
.
Мы уверены, что на вход получаем JsonPrimitive
со строкой, поэтому не будет проверять это (можно было бы использовать метод isJsonPrimitive()
). Дальнейшая обработка полученных данных тривиальна, не будем на ней задерживаться.
Гном
Настало время восстановить данные о гноме. Делаем это так:
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;
}
}
Опять же, некоторые проверки опущены для лаконичности. Разберем по частям.
Мы знаем, что информация о гноме представлена в виде JsonObject
, поэтому преобразуем входные данные к этому типу, не проверяя.
JsonObject jsonObject = json.getAsJsonObject();
Извлекаем возраст, используя сначала метод get()
, который вернет нам JsonElement
со значением указанного свойства «age», а затем метод getAsInt()
, так как возраст имеет целочисленный тип.
dwarf.setDwarfAge(jsonObject.get("age").getAsInt());
Восстанавливаем данные о бороде в объект типа FacialHair
, используя context.deserialize()
. Как мы помним, контекст осведомлен о том, что для обработки информации о бороде нужно использовать специальный десериализатор.
dwarf.setFacialHair((FacialHair) context.deserialize(jsonObject.get("facialHair"), FacialHair.class));
Получаем значение свойства «weapons» сразу в виде Json-массива. Можно было бы сначала получить JsonElement
методом get(«weapons»), затем проверить на принадллежность к типу массива методом isJsonArray()
, и только затем преобразовать в массив с помощью метода getAsJsonArray()
. Но мы верим в наших гномов и формат их входных данных.
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));
}
}
Для каждого элемента проверяем, относится ли он к типу JsonPrimitive
. Мы помним, что обычное оружие описывается простой строкой, что соответствует данному типу. В таком случае создаем экземпляр обычного оружия, получая его тип методом getAsString()
. В противном случае мы имеем дело с уникальным оружием. Мы обрабатывали его с помощью контекста, используя стандартные механизмы Gson. То же самое делаем и теперь, используя context.deserialize()
.
Заметили, что чего-то не хватает? И не просто «чего-то», а имени гнома! Чтобы завершить восстановление информации о гноме, добавив эту важную деталь, перейдем к последнему десериализатору.
Отряд
Наконец, добавим обработчик для всего отряда гномов:
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;
}
}
Как и при обработке гнома, входные данные мы приводим к типу JsonObject
. Помните, ранее упоминалось, что JsonObject
можно воспринимать как Map<String, JsonElement>
? По аналогии с Map
, у JsonObject
есть метод entrySet()
, возвращающий множество элементов ключ-значение. Как раз с его помощью мы пройдем в цикле по всем записям о гномах.
Значение элемента — это вся информация о гноме, кроме имени. Используем контекст, чтобы десериализовать эту информацию и получить экземпляр класса Dwarf.
Dwarf dwarf = context.deserialize(entry.getValue(), Dwarf.class);
Оставшееся незаполненным имя содержится в ключе элемента. Записываем его в наш объект и — вуаля — информация о гноме полностью восстановлена!
dwarf.setName(entry.getKey());
Home, sweet home
Осталось зарегистрировать наши свежеиспеченные десериализаторы, и можно начинать путешествие «Туда и Обратно». Регистрация абсолютно аналогична регистрации сериализаторов:
Gson gson = new GsonBuilder()
.registerTypeAdapter(DwarvesBand.class, new DwarvesBandDeserializer())
.registerTypeAdapter(FacialHair.class, new FacialHairDeserializer())
.registerTypeAdapter(Dwarf.class, new DwafDeserializer())
.create();
Для проверки сначала преобразуем компанию гномов в Json-строку, затем обратно, и для наглядности выведем результат в виде Json-объекта, полученного с помощью стандартного механизма Gson. Можно убедиться, что никто не забыт и ничто не забыто, все гномы вернулись целые и невредимые!
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));
{
"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) и «Обратно» (из JSON в Java). Каждый раз в наших сериализаторах и десериализаторах мы работали с промежуточным слоем объектов типа JsonElement
, которые любезно предоставлял нам Gson.
И хотя это довольно удобно, но приводит к накладным расходам. Gson дает нам возможность пожертвовать удобством в угоду производительности, исключив промежуточный слой. Сделать это можно, используя для кастомного преобразования не пару JsonSerializer + JsonDeserializer, а реализацию класса TypeAdapter<T>, который как раз предназначен для преобразования в обе стороны. Больше всего нас интересуют два абстрактных метода этого класса — write()
и read()
. Именно они отвечают за кастомные преобразования: write()
отвечает за сериализацию, а read()
— за десериализацию.
Помните, мы бросили оружие гнома на произвол обработки по умолчанию? Давайте исправим эту несправедливость. Объединим имя и происхождение оружия в строку вида «Slasher from Gondolin». И дабы не мелочиться, создадим TypeAdapter
для всего списка оружия, а не только для уникальных экземпляров. Наш класс будет иметь такой вид:
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;
}
}
Теперь мы, по старой схеме, должны уведомить Gson о новом обработчике для списка оружия, вызвав метод .registerTypeAdapter()
. Однако, есть тут загвоздка. Первый параметр метода — это тип данных, для которого регистрируется обработчик, а оружие гнома у нас реализовано обычным списком: List<Weapon>
. И мы явно не хотим, чтобы все другие списки обрабатывались нашим TypeAdapter'ом. Нужно как-то указать, что он предназначен только для списка оружия, передав параметризованный тип. Для этого в Gson используется специальный хитрый класс — TypeToken<T>. С его помощью мы может получить нужный нам параметризованный тип следующим образом:
Type weaponsListType = new TypeToken<List<Weapon>>(){}.getType();
По сути, мы специально наследуем параметризованный класс TypeToken
анонимным классом, чтобы затем методом getGenericSuperclass()
получить параметризующий родителя тип. В нашем случае параметризующий родителя тип — это наш List<Weapon>
. Несколько запутано, но по-другому, увы, никак. Более подробно про получение параметров Generic-классов можно почитать, например, в этой статье.
Ну и дальше — как обычно:
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();
Осталось только изменить код сериализации и десериализации гнома, передав управление по обработке оружия контексту с указанием типа обрабатываемого значения:
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);
...
}
}
Вот и все, адаптер подключен. Ах да, осталось еще реализовать его. Как обычно, под спойлером — полный код, который далее разберем подробнее по частям.
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;
}
}
И снова Туда
Итак, за преобразование «Туда» отвечает метод write()
. Его код:
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();
}
Мы видим в параметрах метода экземляр класса JsonWriter и наш список оружия. JsonWriter
позволяет создавать выходной JSON в потоковом режиме. Для начала — нам нужен массив, где мы будем хранить данные об оружии.
out.beginArray();
...
out.endArray();
Эти команды, по сути, отвечают за расстановку квадратных скобок (как, собственно, и обозначаются массивы в JSON). Так как на выходе мы хотим получить массив, то в начале метода начинаем его, а в конце — заканчиваем. Тут все довольно просто. Аналогично используются методы <codebeginObject()</code и <codeendObject()</code для создания объектов.
Далее, в случае с обычным оружием, мы просто записываем в массив значение примитивного типа (строковое), вызвав метод 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<Weapon> 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<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;
}
Мы знаем, что арсенал гнома представлен массивом, в котором содержатся объекты (для уникальных экземпляров) и строки (для обычных). Следовательно, обрабатывая каждый элемент массива, мы проверяем тип начального узла этого элемента. Для обработки строк и объектов у нас созданы методы, которые мы и вызываем. Прочие типы просто пропускаем методом 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<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("nnDeserialized:");
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
И жили они долго и счастливо.
Конец.
Автор: TrebleSnake