Урок 14. Знакомство с форматом JSON. Парсинг JsonObject на примере объекта User
В этом уроке мы с вами более подробно познакомимся с форматом JSON . А потом из этого формата получим наш User объект.
JSON стал общепринятым форматом для обмена данными в клиент-серверных приложения. Он является универсальным форматом для обмена данными.
Представьте, что вам нужно создать систему приложений, в которую входит, сервер, web -клиент, ios -клиент, android -клиент. У всех технологий разные языки,
разные особенности. Поэтому сервер отвечает в формате JSON , а клиенты на своей стороне приводят к нужному формату ответы. Подробнее про JSON формат можно
почитать здесь, но на данном этапе ваших знаний уже вполне хватит.
JSONObject, работа с JSON в java коде
Давайте создадим новый класс в пакете network и назовём его JsonParser для преобразования Json -строк в необходимые нам объекты. Добавим в него один
метод, который назовём getUser(String response) :
Мы будем использовать этот метод внутри класса HttpClient , передавая в него ответ от сервера в формате String , возвращая объект User . Давайте
разберём преобразование, рассмотрим структура хранения JSON . В прошлом уроке мы в конце вывели ответ от сервера в log и увидели очень большой объект JSON .
На данном этапе нам необходимы только те поля, которые мы отображаем на экране. Поэтому опустим остальные поля и оставим только те, которые нам нужны:
Видим, что наш JSON полностью повторяет структуру нашего объекта User . Теперь можно приступить к парсингу (преобразованию) данных.
Первое, что необходимо сделать – это создать JSON объект из строки:
Видим, что AndroidStudio подчёркивает эту строку, указывая что надо обработать исключение или добавить его к сигнатуре метода. Можем нажать alt + enter
и увидеть два этих варианта решения проблемы. Добавим исключение к сигнатуре, чтобы обработать исключения в клиентском коде. Для этого выберем вариант Add exception to method signature :
JSONObject представляет из себя структуру типа HashMap (ключ – значение).
Т.е. чтобы получить значения поля id нам необходимо выполнить вызов метода
userJson.getLong(«id») (получить значение типа long по ключу (названию поля) id ). Давайте сделаем тоже самое для каждого поля.
После этого метод будет выглядеть так:
Т.е. вот так по названию поля мы можем достать его значение. Нам попался самый простой случай, где наши значения являются примитивными типами. На практике мы
поработаем с массивами элементов и вложенными объектами.
Давайте создадим объект User , передав ему в конструктор все поля, которые мы только что достали из userJson :
Вот и всё! Теперь давайте добавим использование нашего класса внутри класса HttpClient .
Добавили JsonParser как final поле в классе HttpClient и инициализируем его в конструкторе.
После этого добавили строку User user = jsonParser.getUser(response); в конец метода getUserInfo и поменяли возвращаемый тип на User и добавили новое
исключение JSONException в сигнатуру метода. Отлично, теперь изменим код в UserInfoActivity и запустим приложение.
в UserInfoActivity нам нужно изменить метод loadUserInfo :
Возвращаемый тип Asynctask со String на User .
Добавили в catch блок перехват исключения типа IOException | JSONException .
В onPostExecute мы заменили вызов Log.d() на вызов реального метода отображения displayUserInfo(user) .
Давайте запустим наше приложение:
Всё работает! Только видим, что наш ник выводится без символа @ . Давайте изменим метод getNick() в классе User . Будем добавлять в него символ @ самостоятельно, т.к. сервер этого не сделал.
Запустим наше приложение и увидим, что теперь с форматом поля nick всё в порядке:
Источник
Gson или «Туда и Обратно»
Недавно мне пришлось поработать с библиотекой Google Gson, предназначенной для преобразования Java-объектов в текстовый формат JSON (сериализация) и обратного преобразования (десереализация). Часто при работе с Gson хватает стандартных настроек библиотеки, но бывают случаи (в том числе мой), когда необходимо кастомизировать процессы преобразований.
Поработав с Gson, я решил написать этот туториал, который иллюстрирует принципы работы с библиотекой на примере. Пост получился сравнительно длинный, но дробить его не хочется из-за логической связности повествования.
Для начала нужно выбрать какую-нибудь предметную область. Скажем, не знаю, почему-то приходят в голову мысль про отряд гномов. Собственно, почему бы и нет?
Да, весь код, задействованный в статье, можно найти на GitHub: https://github.com/treble-snake/gson.dwarves
Изображения, кроме диаграммы классов, позаимствованы из цикла статей о Gson на сайте http://www.javacreed.com.
Введение
О гномах
Итак, с «отрядом» понятно — это некое множество гномов. Но что насчет самих гномов? Самая важная деталь, характеризующая гнома — это, конечно, борода. Можно долго расписывать особенности и классификации гномьих бород, но для простоты определим три параметра: есть ли у гнома усы, есть ли борода, и какого они цвета. Далее, имя и возраст — куда ж без них. Добавим еще что-нибудь личного, скажем, что гном ел на обед. Ну и, наконец, оружие. Оружия у гнома может быть много, причем оно может быть простое, а может быть уникальное, имеющее собственное имя и происхождение.
В итоге получается примерно так:
Для краткости приведу все классы в одном листинге:
Проинициализируем нашу гномью компанию, добавив трех участников (все действующие лица вымышлены, а совпадения случайны):
По умолчанию
Итак, мы хотим получить информацию о наших гномах в формате JSON. Попробуем самый простой способ — использовать стандартные параметры библиотеки Gson, создав экземпляр одноименного класса и вызвав метод toJson() .
Собственно, экземпляр класса Gson можно было создать и через оператор new , но тогда выходной JSON был бы не отформатирован, что хорошо для обмена данными между приложениями (быстрее формируется, меньше весит), но не здорово для человеческого восприятия. Поэтому мы использовали специальный GsonBuilder, вызвав метод setPrettyPrinting() , который позволил лицезреть выходной JSON в следующем виде:
Что ж, с этим уже можно работать, однако, если подумать, то есть несколько замечаний:
- Что за дурацкое название свойства — «dwarfAge»? И так понятно, что речь идет о гноме. Просто «age» смотрелось бы куда лучше.
- Пожалуй, информация про обед не так уж и важна. Можно обойтись без нее.
- Описание бороды какое-то сухое, такого допускать нельзя. Описывать ее нужно законченным предложением, то есть строкой, например: «Red beard and mustache» или «Black mustache».
- Зачем нам заводить объект с единственным свойством «type» для обычного оружия? Обойдется просто строкой.
Если учесть все замечания, то мы хотим видеть информацию о гноме в таком формате:
Аннотации
Gson предоставляет нам несколько полезных аннотаций для настройки сериализации. Посмотрим, смогут ли они помочь нам.
С первой проблемой — да, изменить выходное имя свойства мы можем, добавив аннотацию SerializedName к соотв. полю класса. То есть, сделав так:
Мы получим на выходе свойство с именем «age» вместо «dwarfAge».
Уже неплохо, идем дальше. Нужно исключить поле lunch . Во-первых, сделать это можно, добавив к нему ключевое слово transient, в таком случае поле не будет учитываться при сериализации. Но не факт, что это правильный путь. То, что информация про обед не нужна нам здесь, не значит, что она не нужна при какой-то иной сериализации.
Другой путь — использование аннотации Expose. Она работает только в паре с методом GsonBuilder.excludeFieldsWithoutExposeAnnotation() , который исключает из обработки все поля, не имеющие аннотации Expose. Но, выходит, чтобы исключить одно поле, нам нужно добавить аннотации ко всем остальным полям. Не слишком удобно, верно?
Свой сериализатор
Более гибкий способ — создать свой класс, производящий сериализацию объектов определенного типа. Для этого необходимо реализовать интерфейс JsonSerializer , где T — тип обрабатываемых объектов. Рассмотрим единственный метод serialize() интерфейса:
Он принимает три параметра:
- T src — собственно, сериализуемый объект;
- Type typeOfSrc — тип сериализуемого объекта;
- JsonSerializationContext context — контекст сериализации; интерфейс JsonSerializationContext также является функциональным и содержит 1 метод, тоже serialize() ; его стоит использовать для обработки непримитивных данных, входящих в сериализуемый объект (и мы это сделаем чуть ниже); контекст наследует все настройки (в т.ч. зарегистрированные сериализаторы и т.п.) исходного Gson-объекта.
Возвращаемый тип данных метода — JsonElement . Это абстрактный класс, имеющий 4 реализации, изображенные на рисунке ниже:
- JsonNull — собственно, представление для null
- JsonPrimitive — представление примитивных типов вроде строк, чисел и т.д.
- JsonArray — множество объектов типа JsonElement ; можно рассматривать как List ; элементы могут быть любой из реализаций JsonElement , причем поддерживаются смешанные типы;
- JsonObject — множество пар ключ-значение, где ключ — это строка, а значение — опять же объект типа JsonElement ; аналогично структуре Map .
На рисунке ниже изображен пример сочетания типов:
Время сериализовать гномов
Итак, довольно теории, давайте же наконец сериализовать!
Сперва сколько у нас типов данных, требующих кастомной обработки.
Во-первых, это, конечно, сам класс, описывающий гнома — Dwarf .
Во-вторых, класс бороды и усов — FacialHair .
Еще сюда можно отнести Weapon и особенно UniqueWeapon , но оставим его пока на попечение обработки по умолчанию.
Соответственно, нам нужны две реализации JsonSerializer . Выглядят они вполне аналогично:
Чтобы при обработке гномов Gson использовал наши сериализаторы, нужно зарегистрировать его с помощью метода registerTypeAdapter() класса GsonBuilder следующим образом:
Борода и усы
Реализуем для начала обработку бороды и усов. Ниже приведен полный код, который далее разберем подробней:
Все довольно просто. Так как информацию о бороде и усах мы сводим к одной строке, то результатом работы метода serialize() должен являться объект JsonPrimitive , содержащий нужную строку.
Например, если у гнома нет ни бороды, ни усов, можно поставить под сомнение его отношение к гномьему роду:
В ином случае, используя довольно тривиальный алгоритм, получим из исходных данных строку нужного нам вида, и также создадим на её основе экземпляр JsonPrimitive . И да, примем за данность, что входной объект и цвет волос у нас всегда проинициализированы, чтобы не усложнять код проверками, совершенно неважными для обучающих целей статьи.
Сам гном
Теперь реализуем обработку гнома целиком (также опустим проверки):
Разберем этот код по частям. Так как в результате мы должны получит JSON-объект, то создаем переменную соответствующего типа:
Затем с помощью метода addProperty() заносим в наш объект данные примитивных типов (не создавая при этом промежуточный JsonPrimitive -объект). Передаем в метод два параметра: первый — ключ, то есть название свойства JSON-объекта, второй — собственно, значение этого свойства. Здесь-то мы и задаем имя свойства «age» вместо «dwarfAge», а также исключаем из результата информацию про обед — просто не добавляя её в результирующий объект.
Далее нам нужно добавить данные о бороде. Для этого мы используем метод serialize() контекста — как говорилось ранее, контекст осведомлен о зарегистрированных сериализаторах, поэтому для класса FacialHair применит наш FacialHairSerializer . Получившийся JsonElement мы добавляем к нашему объекту методом add(), указав нужное имя свойства.
Осталось только добавить информацию об оружии гнома. Так как никаких символьных ключей для единиц оружия у нас не предусмотрено, то для их хранения создаем экземпляр JsonArray и добавляем его в наш объект с помощью того же метода add().
Теперь нужно наполнить созданный массив элементами. У класса JsonArray тоже есть метод add(), но он принимает только один параметр типа JsonElement , что и логично — ключ в данном случае не нужен. При добавлении обычного оружия создаем JsonPrimitive на основе строки, а уникальное сериализуем с помощью контекста. В данном случае сработает стандартный механизм сериализации, потому что никаких обработчиков для класса UniqueWeapon мы не регистрировали.
Результат
Наконец, используем плод нашего труда по прямому назначению:
Смотрим, что у нас получилось:
Последний штрих
Единственное, что хотелось бы изменить — все гномы у нас являются элементами массива, который хранится в свойстве «dwarves». Это как-то несолидно, да и избыточно — мы же знаем, что речь идет о гномах, так? Пусть каждый гном будет отдельным свойством JSON-объекта, где ключ — имя гнома. Например:
Скорее всего, вы уже и сами можете представить, что нужно сделать, чтобы воплотить этот финальный штрих в жизнь. Но на всякий случай:
1. Добавляем сериализатор для всей компании гномов:
2. Убираем из сериализатора гнома (класс DwarfSerializer ) информацию об имени, удалив строку:
3. Регистрируем сериализатор отряда, добавив вызов метода registerTypeAdapter() класса GsonBuilder :
Можно смело отправляться за синие горы, за белый туман!
Обратно
Вернувшись из JSON-приключения, отряд гномов, естественно, хочет преобразоваться обратно в уютные Java-объекты. Для обратного преобразования, то есть десериализации, у Gson есть метод fromJson() . Он принимает два параметра: данные в нескольких форматах (в т.ч. String , который мы и будем использовать) и тип возвращаемого результата. Однако, если мы попытаемся просто создать объект Gson и вызвать этот метод, как показано ниже, то получим экземпляр класса DwarvesBand с пустым списком гномов:
Это естественно, ведь для преобразования мы использовали собственные алгоритмы, и настроенный по умолчанию Gson не знает, как обрабатывать наш формат. Поэтому, абсолютно аналогичным образом, мы должны создать специальные десериализаторы и указать библиотеке, что для обработки информации о гномах нужно использовать именно их. Как вы уже, возможно, догадались, для их создания нужно реализовать интерфейс JsonDeserializer и его единственный метод deserialize().
Принимаемые параметры:
- JsonElement json — Json-элемент, из которого нужно восстановить данные;
- Type typeOfT — тип объекта, который должен получиться в результате;
- JsonDeserializationContext context — контекст десериализации; по аналогии с JsonSerializationContext , интерфейс JsonDeserializationContext содержит один метод deserialize() ; этот контекст наследует все настройки Gson-объекта
Возвращаемый тип данных — параметризуется.
Приступим!
Борррода!
Начнем с малого. Восстановим данные о бороде и усах. Полный код десериализатора:
Да, по-хорошему, стоило бы проверять входные данные более тщательно, но примем за данность, что они корректны, дабы не усложнять код примеров.
Самое важная строка в этом методе:
Метод getAsString() преобразует содержимое JsonElement в строку, если применяется к элементу типа JsonPrimitive , содержащему валидную строку, или к JsonArray , содержащему только один такой элемент типа JsonPrimitive . В ином случае метод выбросит исключение. Аналогично работают все методы вида getAs
Мы уверены, что на вход получаем JsonPrimitive со строкой, поэтому не будет проверять это (можно было бы использовать метод isJsonPrimitive() ). Дальнейшая обработка полученных данных тривиальна, не будем на ней задерживаться.
Настало время восстановить данные о гноме. Делаем это так:
Опять же, некоторые проверки опущены для лаконичности. Разберем по частям.
Мы знаем, что информация о гноме представлена в виде JsonObject , поэтому преобразуем входные данные к этому типу, не проверяя.
Извлекаем возраст, используя сначала метод get() , который вернет нам JsonElement со значением указанного свойства «age», а затем метод getAsInt() , так как возраст имеет целочисленный тип.
Восстанавливаем данные о бороде в объект типа FacialHair , используя context.deserialize() . Как мы помним, контекст осведомлен о том, что для обработки информации о бороде нужно использовать специальный десериализатор.
Получаем значение свойства «weapons» сразу в виде Json-массива. Можно было бы сначала получить JsonElement методом get(«weapons»), затем проверить на принадлежность к типу массива методом isJsonArray() , и только затем преобразовать в массив с помощью метода getAsJsonArray() . Но мы верим в наших гномов и формат их входных данных.
Осталось пройтись по массиву, восстанавливая данные об оружии:
Для каждого элемента проверяем, относится ли он к типу JsonPrimitive . Мы помним, что обычное оружие описывается простой строкой, что соответствует данному типу. В таком случае создаем экземпляр обычного оружия, получая его тип методом getAsString() . В противном случае мы имеем дело с уникальным оружием. Мы обрабатывали его с помощью контекста, используя стандартные механизмы Gson. То же самое делаем и теперь, используя context.deserialize() .
Заметили, что чего-то не хватает? И не просто «чего-то», а имени гнома! Чтобы завершить восстановление информации о гноме, добавив эту важную деталь, перейдем к последнему десериализатору.
Отряд
Наконец, добавим обработчик для всего отряда гномов:
Как и при обработке гнома, входные данные мы приводим к типу JsonObject . Помните, ранее упоминалось, что JsonObject можно воспринимать как Map ? По аналогии с Map , у JsonObject есть метод entrySet() , возвращающий множество элементов ключ-значение. Как раз с его помощью мы пройдем в цикле по всем записям о гномах.
Значение элемента — это вся информация о гноме, кроме имени. Используем контекст, чтобы десериализовать эту информацию и получить экземпляр класса Dwarf.
Оставшееся незаполненным имя содержится в ключе элемента. Записываем его в наш объект и — вуаля — информация о гноме полностью восстановлена!
Home, sweet home
Осталось зарегистрировать наши свежеиспеченные десериализаторы, и можно начинать путешествие «Туда и Обратно». Регистрация абсолютно аналогична регистрации сериализаторов:
Для проверки сначала преобразуем компанию гномов в Json-строку, затем обратно, и для наглядности выведем результат в виде Json-объекта, полученного с помощью стандартного механизма Gson. Можно убедиться, что никто не забыт и ничто не забыто, все гномы вернулись целые и невредимые!
В обе стороны
Итак, мы с вами рассмотрели путешествие «Туда» (из Java в JSON) и «Обратно» (из JSON в Java). Каждый раз в наших сериализаторах и десериализаторах мы работали с промежуточным слоем объектов типа JsonElement , которые любезно предоставлял нам Gson.
И хотя это довольно удобно, но приводит к накладным расходам. Gson дает нам возможность пожертвовать удобством в угоду производительности, исключив промежуточный слой. Сделать это можно, используя для кастомного преобразования не пару JsonSerializer + JsonDeserializer, а реализацию класса TypeAdapter , который как раз предназначен для преобразования в обе стороны. Больше всего нас интересуют два абстрактных метода этого класса — write() и read() . Именно они отвечают за кастомные преобразования: write() — за сериализацию, а read() — за десериализацию.
Помните, мы бросили оружие гнома на произвол обработки по умолчанию? Давайте исправим эту несправедливость. Объединим имя и происхождение оружия в строку вида «Slasher from Gondolin». И дабы не мелочиться, создадим TypeAdapter для всего списка оружия, а не только для уникальных экземпляров. Наш класс будет иметь такой вид:
Теперь мы, по старой схеме, должны уведомить Gson о новом обработчике для списка оружия, вызвав метод .registerTypeAdapter() . Однако, есть тут загвоздка. Первый параметр метода — это тип данных, для которого регистрируется обработчик, а оружие гнома у нас реализовано обычным списком: List . И мы явно не хотим, чтобы все другие списки обрабатывались нашим TypeAdapter’ом. Нужно как-то указать, что он предназначен только для списка оружия, передав параметризованный тип. Для этого в Gson используется специальный хитрый класс — TypeToken . С его помощью мы можем получить нужный нам параметризованный тип следующим образом:
По сути, мы специально наследуем параметризованный класс TypeToken анонимным классом, чтобы затем методом getGenericSuperclass() получить параметризующий родителя тип. В нашем случае параметризующий родителя тип — это наш List . Несколько запутано, но по-другому, увы, никак. Более подробно про получение параметров Generic-классов можно почитать, например, в этой статье.
Ну и дальше — как обычно:
Осталось только изменить код сериализации и десериализации гнома, передав управление по обработке оружия контексту с указанием типа обрабатываемого значения:
Вот и все, адаптер подключен. Ах да, осталось еще реализовать его. Как обычно, под спойлером — полный код, который далее разберем подробнее по частям.
И снова Туда
Итак, за преобразование «Туда» отвечает метод write() . Его код:
Мы видим в параметрах метода экземпляр класса JsonWriter и наш список оружия. JsonWriter позволяет создавать выходной JSON в потоковом режиме. Для начала — нам нужен массив, где мы будем хранить данные об оружии.
Эти команды, по сути, отвечают за расстановку квадратных скобок (как, собственно, и обозначаются массивы в JSON). Так как на выходе мы хотим получить массив, то в начале метода начинаем его, а в конце — заканчиваем. Тут все довольно просто. Аналогично используются методы value() :
А для уникального оружия создаем объект и записываем в него две пары ключ-значения, вызывая поочередно методы name() и value() .
Вот и всё, массив с оружием записан.
И опять Обратно
Мы довольно лихо преобразовали наше оружие в JSON-массив со смешанным типом данных, не так ли? И теперь настало время преобразовать его обратно. И тут нас ждет небольшая проблема. Итак, метод read() принимает один параметр:
Класс JsonReader занимается извлечением данных из Json, и тоже в формате потока. Поэтому мы должны последовательно перебрать все «узлы», соответствующим образом их обработав.
По аналогии с записью, объекты и массивы обрабатываются методами beginObject() / endObject() и beginArray() / endArray() .
Cвойства объектов мы перебираем методом nextName() , их значения — методом next
Но все это хорошо, если у нас есть строгий формат данных, с определенной последовательностью элементов. Тогда мы знаем, когда открывать массив, когда объект, и так далее. В нашем же случае мы имеем дело со смешанным типом данных массива, где Json-объекты и строки могут идти в любом порядке. К счастью, у GsonReader есть еще метод peek() , который возвращает тип следующего узла, не обрабатывая его.
Таким образом, общий вид метода read() у нас получится таким:
Мы знаем, что арсенал гнома представлен массивом, в котором содержатся объекты (для уникальных экземпляров) и строки (для обычных). Следовательно, обрабатывая каждый элемент массива, мы проверяем тип начального узла этого элемента. Для обработки строк и объектов у нас созданы методы, которые мы и вызываем. Прочие типы просто пропускаем методом skipValue() .
Метод создания обычного оружия крайне прост:
Просто получаем строку, в которой содержится тип оружия, методом nextString() и создаем на ее основе объект.
С уникальным оружием — несколько сложнее:
Мы заходим в объект и перебираем все его свойства с помощью метода nextName() . Для свойств с именами «name» и «type» у нас есть алгоритмы обработки — мы создаем на их основе экземпляры обычного и уникального оружия. Остальные свойства (буде таковые найдутся), опять же, пропускаем.
Таким образом, десериализация арсенала гнома с помощью TypeAdapter готова.
На всякий случай — проверим, всё ли в порядке.
Послесловие
Вот и подошло к концу путешествие из Java в JSON и обратно. На этом позвольте откланяться, дорогой читатель. Надеюсь, вам было интересно.
Напомню несколько ссылок, которые могут пригодиться:
Источник