Цирцея использовала синтаксический анализ библиотеки 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.