Всем привет!

Во время нашей работы часто приходится сталкиваться с таким форматом обмена данных как 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))

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

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

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


  1. Ninil
    12.10.2023 16:54

    Если известная схема JSON'а, то проще определить case class и потом с помощью net.liftweb.json._ извлечь одной строчкой его.