NAUMEN
Компания
41,05
рейтинг
1 августа 2014 в 14:56

Разработка → Gson или «Туда и Обратно» tutorial

Недавно мне пришлось поработать с библиотекой 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 в следующем виде:
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
    }
  ]
}


Что ж, с этим уже можно работать, однако, если подумать, то есть несколько замечаний:
  1. Что за дурацкое название свойства — «dwarfAge»? И так понятно, что речь идет о гноме. Просто «age» смотрелось бы куда лучше.
  2. Пожалуй, информация про обед не так уж и важна. Можно обойтись без нее.
  3. Описание бороды какое-то сухое, такого допускать нельзя. Описывать ее нужно законченным предложением, то есть строкой, например: «Red beard and mustache» или «Black mustache».
  4. Зачем нам заводить объект с единственным свойством «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);

Смотрим, что у нас получилось:
Выходной 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": []
    }
  ]
}


Последний штрих

Единственное, что хотелось бы изменить — все гномы у нас являются элементами массива, который хранится в свойстве «dwarves». Это как-то несолидно, да и избыточно — мы же знаем, что речь идет о гномах, так? Пусть каждый гном будет отдельным свойством JSON-объекта, где ключ — имя гнома. Например:
{
  "Kori": {
    "age": 60,
    "facialHair": "red mustache",
    "weapons": [ ... ]
  }, 
  ...
}

Скорее всего, вы уже и сами можете представить, что нужно сделать, чтобы воплотить этот финальный штрих в жизнь. Но на всякий случай:
Реализация
1. Добавляем сериализатор для всей компании гномов:
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);
		...
	}
}

Вот и все, адаптер подключен. Ах да, осталось еще реализовать его. Как обычно, под спойлером — полный код, который далее разберем подробнее по частям.
Полный код TypeAdapter'а
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("\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 и обратно. На этом позвольте откланяться, дорогой читатель. Надеюсь, вам было интересно.
Напомню несколько ссылок, которые могут пригодиться:



И жили они долго и счастливо.
Конец.
Автор: @TrebleSnake
NAUMEN
рейтинг 41,05
Компания прекратила активность на сайте

Комментарии (24)

  • +5
    А почему Вы выбрали для работы именно gson? Почему не jackson? На основании чего Вы выбрали конкретно эту библиотеку?
    • +3
      Всегда удивляли такие вопросы…

      А почему надо было выбрать jackson? На основании чего Вы выбрали конкретно эту библиотеку?
      • +2
        Вас удивляет, что люди не берут первый попавшийся вариант и хотят знать отличие от популярных альтернатив?
        • +2
          Нет, меня удивляет такая вот дилетантская постановка вопроса.
          Библиотек море и каждый использует то, что ему удобно/нравится.
          Если можете сказать что-то за jackson, так и говорите:
          «Почему не jackson? Он быстрее/выше/сильнее.
          У него есть поддержка того-то и того-то, а gson ничего подобного нет».
          • +1
            Jackson гораздо популярнее gson. Посему автор вопроса вполне справедливо посчитал, что автор статьи имел возможность ознакомиться с обеими альтернативами, и поделится с нами результатами своего сравнительного анализа. Почитать этот анализ было бы гораздо интереснее, нежели никчемные ответы на вопросы, которые не Вам задавали, и не менее скучные возгласы на тему того, что и как часто Вас удивляет.
      • 0
        Потому что это стандартно встающий вопрос перед разработчиком. Вот есть n библиотек для работы с json. Какую же выбрать?! Какая лучше? Какой удобнее пользоваться? Какая быстрее? Производительность маршалинга и демаршалинга в большинстве очень важна.
        • +1
          Если производительность важна, надо выбирать библиотеку с API SAX-типа.
          • 0
            Это спасет, если вы собираетесь парсить гигабайтные json'ы. А если вы парсите много мелких json файлов, то не вижу никакого преимущества библиотек с API SAX-типа.
            • 0
              Еще можно парсить мегабайтные json'ы на android. В этом случае конечно же лучше выбрать Jackson
    • 0
      Немного странно видеть такой вопрос к обучающему материалу. Если вы хотели получить в ответ сравнительный анализ этих библиотек — извините, с jackson мне не приходилось плотно работать. Gson устраивал меня по всем параметрам, плюс — имел, на мой взгляд, более удобный API.
      Но на тему сравнения подобных инструментов есть довольно много информации, правда, в основном на английском. Если вам действительно интересен такой обзор на русском языке — возможно, он заслуживает отдельной статьи.
  • 0
    Была у меня потребность генерировать почти-json (json, расширенный тем, что значения некоторых полей были бы кусочком кода на js).
    К сожалению, попытка использовать gson для этого не увенчалась успехом: всех его богатых возможностей кастомизации оказалось недостаточно.
    Все уперлось в конечном итоге в ограниченные возможности JsonWriter: String там, как положено в json, всегда в кавычках и с соответствующим escaping'ом.
    • 0
      И как вышли из положения?
      • 0
        выход — писать ручками, к сожалению,
        так как задача у меня была более узкая (генерация json подобного input'а для конкретной библиотеки), то это оказалось вполне незатратно
  • –3
    За статью спасибо, но перенос { } на новую строку в java ужасно бесит:(
    • –1
      Почему бы не настроить Codestyle в своей IDE?
      • +1
        У меня в IDE всё нормально. И по дефолту и в Eclipce и intellij idea скобки не переносятся ибо так в java заведено
        Это у автора в IDE что то не так с форматированием кода.
  • 0
    А зачем было выводить имя гнома в ключ? Чтобы люди без вашего адаптера задолбались парсить это?
    Нет, я понимая когда пишут свои адаптеры для типов, типа ObjectID org.bson.types.ObjectId для монги. Но вот так адаптеры под классы.
    • 0
      Не забывайте, статья — обучающий материал. Я сделал это именно с целью показать возможный способ использования в качестве ключа самих данных.
      И кстати, да, на практике был случай, когда это действительно было удобно — при передачи данных в систему на Scala + Mongo.
      • 0
        И чем это было удобно? Обычный сериализатор если ему передать такие данные будет думать что это название объекта и будет пытаться найти класс с таким именем — Orin или Kodi и т.д.

        Прогоните свой json с именами в виде ключей хотябы тут json2csharp.com/ и убедитесь.
        Так же если отправить его в Rails он сериализует каждого гнома в отдельный хеш с именем гнома, хотя должен был бы в масив с хешами чтобы удобней по ним можно было проходить.
  • –1
    Простите за нытье, но меня начинает раздражать потобный стиль заголовков
    «Gson или «Туда и Обратно»» или (точно не помню) «Не стреляйте себе в ногу. О разработке».

    Что это? О чем это? Нужно ли мне это? Вот я вижу, что-то про json. Ну вроде я рабаю с json. Зашел. А не, это про другое. Вышел.
    И таких постов все больле и больше.
    • –2
      Простите и меня за озвучивание очевидного, но, помимо заголовка, у статьи есть анонс, выводящийся до хабраката. В нем, по-моему, довольно четко и лаконично описано, что такое Gson, зачем он нужен, и о чем эта статья. Плюс пометка «tutorial».
  • 0
    Статья большая и интересная, большое спасибо автору за труд.
    А некоторые комментаторы реально похожи на «бабок в очереди».
  • 0
    Для того, чтобы не проставлять Expose каждому полю, есть вот такое решение: stackoverflow.com/a/17733569

    Статья годная, и правильно, что без дроблений на части, спасибо.
    • 0
      Ещё можно объявить ненужные поля как transient.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка