Всем привет!
Во время нашей работы часто приходится сталкиваться с таким форматом обмена данных как JSON, и на данный момент существует большое количество различных библиотек для JSON сериализации. Конечно, для любителей языка программирования Scala, которые хотят использовать преимущества этого языка, тоже есть такая библиотека – о ней и пойдёт речь в данной статье.
Официальная ссылка на библиотеку Circe:
Для тех, кто хочет сразу же посмотреть, что получится в ходе статьи, добро пожаловать на GitHub со всем кодом, который рассмотрим далее.
Circe: Libraries
Для знакомства с Circe в рамках данной статьи нам понадобятся следующие библиотеки:
Circe-core
Сirce-generic
Сirce-parser
В данной статье была использована версия Circe - 0.12.3
Circe: Parsing
Стоит начать с простого парсинга. Для этого создадим метод, который будет превращать обыкновенную строку в JSON-объект.
В Circe это реализовано весьма просто:
def parseToJson(data: String): Either[ParsingFailure, Json] = parse(data)
parse(str) – это метод, который уже находится в Circe и при попытке превратить нашу строку в JSON-объект, он возвращает результат в обертке Either, что позволяет получить JSON-объект или ошибку, которая возникла при парсинге.
Давайте убедимся в том, что это работает. Для примера возьмём актера, его пол и список фильмов, в которых он играл:
val simplyStr: String =
"""
|{
| "name": "Leonardo Dicaprio",
| "sex": "male",
| "films": [
| "Titanic",
| "The Wolf of Wall Street",
| "The Revenant"
| ]
| }
|""".stripMargin
val parseJsonRes: Either[ParsingFailure, Json] = JsonFormatter.parseToJson(simplyStr)
После запуска приложения видим, что все работает корректно:
Right({
"name" : "Leonardo Dicaprio",
"sex" : "male",
"films" : [
"Titanic",
"The Wolf of Wall Street",
"The Revenant"
]
})
Если же в работе мы встретим строку в виде JSON Array, то нам нужно парсить это немного иначе. Для парсинга таких объектов нам понадобится новый метод, который будет выглядеть следующим образом:
def parseToJsonList(data: String): Either[ParsingFailure, List[Json]] =
parse(data).map(json => json.asArray.toList.flatten)
В данном случае мы используем внутренний метод Circe – asArray, который приводит наш JSON к коллекции, а мы уже конвертируем его в нужный нам List.
Для примера возьмем строку, которая будет содержать список актеров. Выглядеть она будет примерно так:
val listSimplyStr: String =
"""
|[
|{
| "name": "Leonardo Dicaprio",
| "sex": "male",
| "films": [
| "Titanic",
| "The Wolf of Wall Street",
| "The Revenant"
| ]
| },
| {
| "name": "Chloe Grace Moretz",
| "sex": "female",
| "films": [
| "The Equalizer",
| "Carrie",
| "Movie 43"
| ]
| }
| ]
|""".stripMargin
val parseJsonListRes = JsonFormatter.parseToJsonList(listSimplyStr)
Давайте посмотрим, что показывает нам консоль при запуске приложения:
Right(List({
"name" : "Leonardo Dicaprio",
"sex" : "male",
"films" : [
"Titanic",
"The Wolf of Wall Street",
"The Revenant"
]
}, {
"name" : "Chloe Grace Moretz",
"sex" : "female",
"films" : [
"The Equalizer",
"Carrie",
"Movie 43"
]
}))
Как можно заметить, в консоль вывелось то, что и ожидалось, а именно – List состоящий из двух объектов типа JSON.
На этом с базовым парсингом мы разобрались и теперь можем перейти к более сложным вещам: к таким понятиям, как декодинг и энкодинг в Circe.
Circe: Decoding and Encoding
Circe, как и любая иная библиотека, работающая с JSON-форматом, умеет конвертировать JSON в класс и обратно. Она предлагает такие виды конвертации, как автоматическая, полуавтоматическая и ручная. Об этом мы и поговорим далее.
Для конвертации Circe использует классы Encoder и Decoder – для кодирования и декодирования соответственно.
Encoding
Для демонстрации конвертации класса в строку нам понадобится следующий метод:
def convertToJsonString[T: Encoder](obj: T): String = Encoder[T].apply(obj).noSpaces
Как видно из кода выше, Circe из коробки предлагает нам готовый вариант решения нашей задачи по конвертации класса в JSON. Для того, чтобы превратить наш JSON в строку, достаточно вызвать noSpace, благодаря которому мы преобразуем JSON в обычную строку.
Decoding
Для демонстрации конвертации строки в класс нам понадобится следующий метод:
def convertToObj[T: Decoder](data: String): Either[circe.Error, T] = decode[T](data)
Здесь, мы возьмем готовый вариант библиотеки без каких-либо изменений. Наши функции выглядят довольно просто, но есть одно «но»: для того, чтобы они заработали, нам необходимо создать специальные конверторы.
Примеры классов для обмена информацией
Создадим пару кейс-классов, которые могли бы использоваться для простейшего обмена информацией:
case class PersonInfo(sex: String, age: Int, marital_status: Option[String])
case class Actor(name: String, info: PersonInfo)
Именно на этих классах мы и будем пробовать различные виды конвертации.
Ручная конвертация
Для того, чтобы продемонстрировать работу ручной конвертации, создадим два трейта, один из которых будет отвечать за превращение нашего кейс-класса в строку (ManualEncoders), а другой –за превращение строки в кейс-класс (ManualDecoders). Далее оба этих трейта объединим в один ManualConverter.
Manual Decoder
ManualDecoders будет выглядеть следующим образом:
trait ManualDecoders {
implicit val personInfoDecoder: Decoder[PersonInfo] = (hCursor: HCursor) =>
for {
sex <- hCursor.get[String]("sex")
age <- hCursor.get[Int]("age")
marital_status <- hCursor.get[Option[String]]("marital_status")
} yield PersonInfo(sex, age, marital_status)
implicit val actorDecoder: Decoder[Actor] = (hCursor: HCursor) =>
for {
name <- hCursor.get[String]("name")
personInfo <- hCursor.get[PersonInfo]("info")
} yield Actor(name, personInfo)
}
Давайте немного поговорим о том, что же здесь происходит. Мы создаём имплиситы для каждого кейс-класса, которые будут подтягиваться в наш convertToObj, чтобы он понимал – как именно конвертировать эти объекты. Для этого нам необходимо задействовать объект типа Hcurcor.
Если представить процесс его работы, то Hcursor представляет собой некую точку, которая может ходить по-нашему JSON-объекту как по дереву. После того, как она пробежалась по нему, создаются взаимосвязи в виде ключ-значение. Тем самым Hcursor может помочь нам извлечь необходимую информацию. По сути, когда мы говорим о Circe, любой процесс имеет этот курсор, просто ранее он был скрыт под верхнеуровневым API. Но при создании декодера нам потребуется спустится на уровень ниже, где он нам и понадобится.
После создания курсора, используя for-comprehension, мы начнём создавать наш объект. Просим выдать некие значения по определенным ключам, которые были найдены после просмотра дерева Hcursor и превратить полученное значение в необходимый нам тип данных. Получив все значения, создаём из них необходимый нам класс.
Наш ручной Decoder готов и мы можем двигаться дальше.
Manual Encoder
Теперь создадим ручной Encoder. Он будет выглядеть следующим образом:
trait ManualEncoders {
implicit val personInfoEncoder: Encoder[PersonInfo] = info =>
Json.obj(
"sex" -> info.sex.asJson,
"age" -> info.age.asJson,
"marital_status" -> info.marital_status.asJson
)
implicit val actorEncoder: Encoder[Actor] = actor =>
Json.obj(
"name" -> actor.name.asJson,
"info" -> actor.info.asJson
)
}
Здесь мы снова создаем имплиситы, которые будут подтягиваться в наш convertToJsonString.
Взяв наш класс PersonInfo и обозначив его info, начнём создавать наш JSON-объект. Этот метод принимает tuples вида (String, Json), таким образом мы создаём ключи в нашем JSON-объекте из строк в левой части, а их значения получаем из полей объектов кейс-классов, приведенных к JSON в правой части.
Наш ручной Encoder также готов.
Manual Converter
Таким образом, все готово для ручного конвертера. Теперь объединим оба наших трейта в готовый конвертер для классов Actor, PersonalInfo и можем использовать его там, где он понадобится:
trait ManualConverter extends ManualEncoders with ManualDecoders
Запустим приложение, чтобы убедится, что все работает:
object CirceApp extends App with ManualConverter {
val actor: Actor =
Actor("Leonardo Dicaprio", PersonInfo("male", 46, None))
// Encoding
val actorJson: String = JsonFormatter.convertToJsonString[Actor](actor)
val infoJson: String = JsonFormatter.convertToJsonString[PersonInfo](actor.info)
// Decoding
val actorObj: Either[circe.Error, Actor] = JsonFormatter.convertToObj[Actor](actorJson)
val infoObj: Either[circe.Error, PersonInfo] = JsonFormatter.convertToObj[PersonInfo](infoJson)
Проверим Encoding:
Convert Obj to Json String:
{"name":"Leonardo Dicaprio","info":{"sex":"male","age":46,"marital_status":null}}
{"sex":"male","age":46,"marital_status":null}
Проверим Decoding:
Convert String to Obj:
Right(Actor(Leonardo Dicaprio,PersonInfo(male,46,None)))
Right(PersonInfo(male,46,None))
Как видно из результатов, все работает корректно.
На этом с ручной конвертацией разобрались, теперь можем переходить к полуавтоматической.
Полуавтоматическая конвертация
Ручная конвертация требует написания большого количества шаблонного кода и для того, чтобы избежать этого, Сirce предлагает полуавтоматическую конвертацию. Нам снова понадобится три трейта, но выглядеть они будут куда проще.
Semi-automatic Decoder
Полуавтоматический декодер выглядит так:
trait SemiAutomaticDecoder {
implicit val personInfoDecoder: Decoder[PersonInfo] = deriveDecoder[PersonInfo]
implicit val actorDecoder: Decoder[Actor] = deriveDecoder[Actor]
}
Здесь всего пару коротких имплиситов для классов, которые нам необходимо конвертировать, но присутствует специальный вид декодера, который нам необходимо импортировать:
import io.circe.generic.semiauto.deriveDecoder
Наш полуавтоматический декодер готов.
Semi-automatic Encoder
Давайте взглянем на Encoder:
trait SemiAutomaticEncoder {
implicit val personInfoEncoder: Encoder.AsObject[PersonInfo] = deriveEncoder[PersonInfo]
implicit val actorEncoder: Encoder.AsObject[Actor] = deriveEncoder[Actor]
}
И его импорты:
import io.circe.generic.semiauto.deriveEncoder
Здесь все то же самое, что и в декодере, только происходит обратный процесс. Наш полуавтоматический энкодер тоже готов.
Semi-automatic Converter
Для полуавтоматического конвертера все готово, остаётся снова объединить оба наших трейта в готовый конвертер:
trait SemiAutomaticConverter extends SemiAutomaticEncoder with SemiAutomaticDecoder
После чего нужно заменить наш ручной конвертер на полуавтоматический:
object CirceApp extends App with SemiAutomaticConverter
Ну, и запустить то же самое приложение, в котором мы использовали ручной конвертер.
Мы получим абсолютно такой же результат:
Convert Obj to Json String:
{"name":"Leonardo Dicaprio","info":{"sex":"male","age":46,"marital_status":null}}
{"sex":"male","age":46,"marital_status":null}
Convert String to Obj:
Right(Actor(Some(Leonardo Dicaprio),Some(PersonInfo(Some(male),Some(46),None))))
Right(PersonInfo(Some(male),Some(46),None))
Semi-automatic Union Converter (Codec)
Также в Circe при создании конвертеров в полуавтоматическом режиме есть возможность не писать отдельно энкодер и декодер: достаточно сделать общий Codec, который представляет собой объединенные вместе энкодер и декодер.
Выглядит он так:
import io.circe.generic.semiauto.deriveCodec
trait SemiAutomaticUnionConverter {
implicit val personInfoEncoder: Codec.AsObject[PersonInfo] = deriveCodec[PersonInfo]
implicit val actorEncoder: Codec.AsObject[Actor] = deriveCodec[Actor]
}
Благодаря чему мы можем не создавать общий трейт для объединения энкодера и декодера, а сразу использовать его, потому что он подходит как для операции превращения классов в JSON, так и обратно.
На этом с полуавтоматической конвертацией мы разобрались и нам остается последний вид конвертации, о котором мы поговорим далее.
Автоматическая конвертация
Предыдущий вид конвертации был достаточно прост, но Circe может упростить этот процесс еще больше.
Для работы автоматической конвертации нам не потребуется создавать каких-либо трейтов с имплиситами, а лишь потребуется одна маленькая деталь. Именно там, где нужно применять метод конвертации, нам нужно импортировать следующее:
import io.circe.generic.auto._
Благодаря этому импорту Circe в автоматическом режиме генерирует все, что необходимо для конвертации.
Уберем все наши ранее используемые трейты и оставим только один импорт автоматического генератора:
import io.circe.generic.auto._
object CirceApp extends App
Попробуем запустить наше приложение и снова убедимся в том, что все отработало как полагается:
Convert Obj to Json String:
{"name":"Leonardo Dicaprio","info":{"sex":"male","age":46,"marital_status":null}}
{"sex":"male","age":46,"marital_status":null}
Convert String to Obj:
Right(Actor(Some(Leonardo Dicaprio),Some(PersonInfo(Some(male),Some(46),None))))
Right(PersonInfo(Some(male),Some(46),None))
Как можно заметить, все на месте и ничего не отличается от вышеиспользуемых конверторов.
Какую конвертацию выбирать – дело за вами. Все они работают, вопрос лишь в том – как вы планируете использовать конвертор для решения своих задач.
Ninil
Если известная схема JSON'а, то проще определить case class и потом с помощью net.liftweb.json._ извлечь одной строчкой его.