Основы библиотеки Circe или простой JSON-парсер на Scala

от автора

Всем привет!

Во время нашей работы часто приходится сталкиваться с таким форматом обмена данных как JSON, и на данный момент существует большое количество различных библиотек для JSON сериализации. Конечно, для любителей языка программирования Scala, которые хотят использовать преимущества этого языка, тоже есть такая библиотека – о ней и пойдёт речь в данной статье.

Официальная ссылка на библиотеку Circe:

Для тех, кто хочет сразу же посмотреть, что получится в ходе статьи, добро пожаловать на GitHub со всем кодом, который рассмотрим далее.

Circe: Libraries

Для знакомства с Circe в рамках данной статьи нам понадобятся следующие библиотеки:

  1. Circe-core

  2. Сirce-generic

  3. С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)

В данном случае мы используем внутренний метод CirceasArray, который приводит наш 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))

Как можно заметить, все на месте и ничего не отличается от вышеиспользуемых конверторов.

Какую конвертацию выбирать – дело за вами. Все они работают, вопрос лишь в том – как вы планируете использовать конвертор для решения своих задач.


ссылка на оригинал статьи https://habr.com/ru/articles/767032/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *