Цирцея использовала синтаксический анализ библиотеки Json в Scala. Сила Circe заключается в том, что он может полиморфно преобразовать Json String в ADT. Однако я испытал разочарование при первом использовании Circe - отчасти потому, что я был новым Scala как языком программирования и прикоснулся к миру функционального программирования. Иногда сообщение об ошибке непрозрачно или существует определенная конфигурация, которую необходимо пройти через исходный код для достижения определенных целей.

Эта статья является продолжением статьи 7 быстрых советов по синтаксическому анализу Json с помощью Circe.

По мере того, как я разрабатывал все больше и больше приложений с помощью Scala и все больше разбирался в парадигме функционального программирования, я хочу поделиться всеми подводными камнями, с которыми я столкнулся при синтаксическом анализе Json с помощью Circe. Это примеры использования, которые я встречал на своем рабочем месте, и способы их решения.

Тип копродукции (суммы) кодирования / декодирования в ADT

Иногда мы хотим декодировать копроизведение в ADT с другим строковым представлением, чем тип копродукта.

Например, у нас есть 4 Дома в Домах Хогвартса в Гарри Поттере.

Мы хотим смоделировать четыре дома как тип сопродукта как ADT и иметь возможность полиморфно кодировать / декодировать соответствующий case object:

case class House(`type` : HouseType)

sealed trait HouseType
case object GodricGryffindor extends Houses
case object SalazarSlyntherin extends Houses
case object RowenaRavenclaw extends Houses
case object HelgaHufflepuff extends Houses

Однако, поговорив с другими командами о контракте, они решают отправить Houses типы в виде Snake_Case.

"Godric_Gryffindor" => GodricGryffindor

Если мы используем circe.generic.semiauto.{deriveEncoder,deriveDecoder}, результатом типа JSON будет GodricGryffindor.

{
  "type" : "GodricGryffindor"
}

Сначала определите экземпляры кодировщика и декодера для House.

import io.circe.generic.semiauto.{deriveEncoder, deriveDecoder}

implicit val houseEncoder:encoder[Houses] = deriveEncoder
implicit val houseDecoder:encoder[Houses] = deriveDecoder

Быстрое преобразование входящих файлов Json и преобразование их в один из файлов case object.

implicit val housesEncoder: Encoder[HouseType] = (obj: HouseType) => obj match {
    case HelgaHufflepuff => Json.fromString("Helga_Hufflepuff")
    case RowenaRavenclaw => Json.fromString("Rowena_Ravenclaw")
    case GodricGryffindor => Json.fromString("Godric_Gryffindor")
    case SalazarSlyntherin => Json.fromString("Salazar_Slyntherin")
  }

  implicit val housesDecoder: Decoder[HouseType] = (hcursor:HCursor) => for {
    value <- hcursor.as[String]
    result <- value match {
      case "Helga_Hufflepuff" => HelgaHufflepuff.asRight
      case "Rowena_Ravenclaw" => RowenaRavenclaw.asRight
      case "Godric_Gryffindor" => GodricGryffindor.asRight
      case "Salazar_Slyntherin" => SalazarSlyntherin.asRight
      case s => DecodingFailure(s"Invalid house type ${s}", hcursor.history).asLeft
    }
  } yield result

Мы можем добиться различия в элементе поля ’type’, немного поработав над определением кодировщика и декодера Coproduct.

val gryffindor = (Houses(`type` = GodricGryffindor)).asJson
println(gryffindor.spaces2)

// {
//  "type" : "Godric_Gryffindor"
// }

Преобразование EpochMillis в Instant в ADT типа продукта

Вы хотите создать класс Currency с полем createdDate.

Пример Currency класса:

case class Currency(id: Int, name:String, description:String, isoCodeAlphabetic:String, createdDate:Instant)

Строка JSON передает createdDate как EpochMillis. Однако вы хотите преобразовать его в Instant, чтобы упростить выполнение любых операций с createdDate.

Пример Currency строки JSON:

{
  "id" : 1,
  "name" : "US Dollars",
  "description" : "United States Dollar",
  "isoCodeAlphabetic" : "USD",
  "createdDate" : 1595270691417
}

Создайте еще один экземпляр кодирования / декодирования, если вы хотите преобразовать определенные члены case class в Circe.

Вам просто нужно создать еще один экземпляр в неявной области видимости для преобразования из Long, EpochMillis в Instant.

implicit val encoder:Encoder[Instant] = Encoder.instance(time => Json.fromLong(time.toEpochMilli))
 implicit val decoder:Decoder[Instant] = Decoder.decodeLong.emap(l => Either.catchNonFatal(Instant.ofEpochMilli(l)).leftMap(t => "Instant"))

Затем создайте кодировщик / декодер для Currency:

implicit val encoderCurrency: Encoder[Currency] = deriveEncoder
implicit val deoderCurrency: Decoder[Currency] = deriveDecoder

Circe посмотрит на неявную область видимости, чтобы проверить, существует ли экземпляр кодировщика / декодера от одного значения к другому. При неявном разрешении Circe может производиться от одного типа к другому, если вы предоставите экземпляр кодировщика / декодера этого типа.

Кодировать / декодировать полиморфный ADT

Давайте определим строку JSON, которую вы хотите получить в этом случае использования:

{
  "houseType" : {
    "type" : "Rowena_Ravenclaw",
    "characteristics" : [
      "Loyal"
    ],
    "animalRepresentation" : "eagle"
  },
  "number" : 12
}

И мы хотим преобразовать его в:

House(RowenaRavenclaw(List(Loyal),eagle),12)

Обратите внимание, что type указывает, в какие имена конструкторов вы хотите преобразовать строку JSON (в данном случае это RowenaRavenclaw).

Декодирование с помощью обычного CirceDecoder вернет класс case, указанный ниже.

House(houseType(`type`: "Rowena_Ravenclaw", List(Loyal),eagle),12)

Как можно полиморфно декодировать строку JSON, сопоставляя член ее поля с именем конструктора?

Есть два пути. Первый будет кодировать и декодировать обычным образом, а второй будет использовать Circe.extras.

Обычное кодирование и декодирование

То, как вы структурируете ADT, имеет огромное значение. Чтобы объяснить обходной путь описанного выше варианта использования, я буду использовать @JsonCodec для автоматического получения кодировщика и декодера для обычного класса case.

Определение типа модели для House и HouseType:

@JsonCodec
case class House(houseType: HousesTypes, number:Int)

trait House
object House {
    @JsonCodec
    case class GodricGryffindor(characteristics:List[String]) extends HousesTypes

    object GodricGryffindor{
      val typeId: String = "Godric_Gryffindor"
    }
    
    case object SalazarSlyntherin extends HousesTypes {
      val typeId: String = "Salazar_Slyntherin"
    }

    @JsonCodec
    case class RowenaRavenclaw(characteristics:List[String], animalRepresentation:String) extends HousesTypes

    object RowenaRavenclaw{
       val typeId: String = "Rowena_Ravenclaw"
    }

    @JsonCodec
    case class HelgaHufflepuff(animalRepresentation:String, colours:String) extends HousesTypes

    object HelgaHufflepuff{
      val typeId: String = "Helga_Hufflepuff"
    }
  
}

В приведенном выше определении ADT мы хотим иметь неявный кодировщик и декодер для HouseType.

Во время кодирования конкретного HouseType мы хотим добавить поле type к строке JSON.

{
  "houseType" : {
    "type" : "Rowena_Ravenclaw", << - We want to append this based on the specific HouseType
    "characteristics" : [
      "Loyal"
    ],
    "animalRepresentation" : "eagle"
  },
  "number" : 12
}

Экземпляр кодировки Circe:

implicit val encoder:Encoder[HousesTypes] =  {
    // deepMerge - insert the encoded Json with another field `type`
    // Basically overriding the current encoder with the `type`
    case obj: GodricGryffindor => obj.asJson deepMerge(Json.obj("type" -> Json.fromString(GodricGryffindor.typeId)))
    case obj: RowenaRavenclaw => obj.asJson deepMerge(Json.obj("type" -> Json.fromString(RowenaRavenclaw.typeId)))

    case obj: HelgaHufflepuff => obj.asJson deepMerge(Json.obj("type" -> Json.fromString(HelgaHufflepuff.typeId)))
    case obj: HousesTypes => Json.obj("type" -> Json.fromString(SalazarSlyntherin.typeId))
  }

Мы хотим получить поле type в строке houseType JSON и декодировать всю строку Json на основе этого типа во время декодирования. Например, Rowena_Ravenclaw будет указывать на RowenaRavenclaw класс дела.

implicit val decoder:Decoder[HousesTypes] = (cursor:HCursor) => for {
    tpe <- cursor.get[String]("type")
    result <- tpe match {
      case GodricGryffindor.typeId => cursor.as[GodricGryffindor]
      case RowenaRavenclaw.typeId => cursor.as[RowenaRavenclaw]
      case HelgaHufflepuff.typeId => cursor.as[HelgaHufflepuff]
      case SalazarSlyntherin.typeId => SalazarSlyntherin.asRight
      case s => DecodingFailure(s"Invalid house type ${s}", cursor.history).asLeft
    }
  } yield result

Использование Circe.extras

У Circe есть специальная библиотека circe.extras, которая решает вопросы кодирования / декодирования полиморфных ADT.

Во-первых, давайте перепишем нашу модель, изменив HouseType на sealed trait:

sealed trait HouseType {
    def `type`: String
  }

  object HouseType {
    case class GodricGryffindor(characteristics:List[String]) extends HouseType {
      override def `type`: String = "Godric_Gryffindor"
    }
    case object SalazarSlyntherin extends HouseType {
      override def `type`: String = "Salazar_Slyntherin"
    }
    case class RowenaRavenclaw(characteristics:List[String], animalRepresentation:String) extends HouseType {
      override def `type`: String = "Rowena_Ravenclaw"
    }
    case class HelgaHufflepuff(animalRepresentation:String, colours:String) extends HouseType {
      override def `type`: String = "Helga_Hufflepuff"
    }
  }

Вы можете установить type в строке JSON в качестве дискриминатора, указать конструктор в конфигурации и неявно объявить конфигурацию.

implicit val houseTypeConfig = Configuration.default.withDiscriminator("type").copy(
      transformConstructorNames = {
        case "GodricGryffindor" => "Godric_Gryffindor" // from `type` on the right transform the case changes to the left
        case "SalazarSlyntherin" => "Salazar_Slyntherin"
        case "RowenaRavenclaw" => "Rowena_Ravenclaw"
        case "HelgaHufflepuff" => "Helga_Hufflepuff"
      }
    )

Я настраиваю конфигурацию для преобразования имен конструкторов. Я хочу преобразовать всю JsonString на основе одного из полей type. Поле JSON type содержит значение в правой части оператора case ('Godric_Gryffindor', 'Salazar_Slyntherin', ...). Я хочу преобразовать строку JSON в класс case в левой части оператора case ('GodricGryffindor', 'SalazarSlyntherin'). Левая часть оператора case будет соответствовать модели, которую мы определили выше.

Затем в процессе кодирования / декодирования используйте deriveConfiguredEncoder и deriveConfiguredDecoder в circe.generic.extras для полиморфного кодирования / декодирования строки JSON:

implicit val house2Encoder = {
    implicit val config = houseTypeConfig
    deriveConfiguredEncoder[HouseType]
}

implicit val house2Decoder = {
  implicit val config = houseTypeConfig
  deriveConfiguredDecoder[HouseType]
}

Тестирование и запуск указанной выше команды:

val ravenClaw: HouseType = RowenaRavenclaw(characteristics = List("Loyal"), animalRepresentation = "eagle")
val ravenClawJson = ravenClaw.asJson
val ravenClawStr = ravenClawJson.noSpaces
println(ravenClaw.asJson.spaces2)
println(decode[HouseType](ravenClawStr).right.get)

Забрать

  • Вы можете кодировать и декодировать определенное поле Json, предоставив экземпляр кодировщика / декодера этих типов. Circe использует неявное разрешение компилятора для преобразования JsonString в желаемый ADT.
  • Используйте CirceExtra для кодирования / декодирования ADT типа Coproduct с помощью circe.generic.extras.Configuration.
  • Используйте deepMerge, чтобы объединить один объект JSON с другим и ввести любые нужные поля.

Вот и все, что касается кодирования и декодирования ADT в Circe!

Я надеюсь, что эта статья поможет вам приступить к работе над вашими следующими проектами по кодированию / декодированию типа ADT с помощью Circe.

Все исходные коды находятся здесь.

Первоначально опубликовано на https://edward-huang.com.