Декодировать json внутри строки json

Я имею дело с API который ожидает объект JSON, где одно из значений (большой двоичный объект) является строковым объектом JSON:

{
    "credential": {
        "blob": "{\"access\":\"181920\",\"secret\":\"secretKey\"}",
        "project_id": "731fc6f265cd486d900f16e84c5cb594",
        "type": "ec2",
        "user_id": "bb5476fd12884539b41d5a88f838d773"
    }
}

Мой класс домена:

case class Credential(access: String, secret: String, projectId: String, userId: String)

Кодировать доменный класс легко:

implicit val encoder: Encoder[Credential] = (a: Credential) => Json.obj(
  "type" -> "ec2".asJson,
  "blob" -> Map("access" -> a.access, "secret" -> a.secret).asJson.noSpaces.asJson,
  "project_id" -> a.projectId.asJson,
  "user_id" -> a.userId.asJson
)

Однако декодирование намного сложнее:

implicit val decoder: Decoder[Credential] = (c: HCursor) => for {
  blobJsonString <- c.get[String]("blob")
  blob <- decode[Json](blobJsonString).left.map(e => DecodingFailure(e.getMessage, c.downField("blob").history))
  access <- blob.hcursor.get[String]("access")
  secret <- blob.hcursor.get[String]("secret")
  projectId <- c.get[String]("project_id")
  userId <- c.get[String]("user_id")
} yield Credential(access, secret, projectId, userId)

Мне не нравится эта реализация, потому что она заставляет меня зависеть от circe-parser и ломать уровень абстракции, предоставляемый кодировщиками/декодерами.

Есть ли способ реализовать декодер, который обычно выполняет двойное декодирование?


person Simão Martins    schedule 03.02.2020    source источник


Ответы (1)


Что ж, поскольку описанный JSON не совсем типичный случай, я не уверен, что можно полностью избежать ручного разбора, но если вы измените класс случая, представляющий эту структуру, вы можете использовать некоторые преимущества, которые предлагает circe. Пожалуйста, найдите пример кода ниже:

import io.circe._
import io.circe.generic.semiauto._
import io.circe.generic.auto._

object CredentialsParseApp {
  case class CredentialsBlob(access: String, secret: String)

  object CredentialsBlob {

    implicit val encoder: Encoder[CredentialsBlob] = {
      val derivedEncoder: Encoder[CredentialsBlob] = deriveEncoder[CredentialsBlob]
      Encoder[String].contramap(blob => derivedEncoder(blob).noSpaces)
    }

    implicit val decoder: Decoder[CredentialsBlob] = {
      val derivedDecoder: Decoder[CredentialsBlob] = deriveDecoder[CredentialsBlob]
      Decoder[String].emap { value =>
        for {
          json <- parser.parse(value).left.map(_.message)
          blob <- json.as(derivedDecoder).left.map(_.message)
        } yield blob
      }
    }
  }

  case class Credentials(blob: CredentialsBlob, project_id: String, `type`: String = "ec2", user_id: String)
  case class Response(credential: Credentials)

  def main(args: Array[String]): Unit = {
    val jsonString =
      """{
         |    "credential": {
         |        "blob": "{\"access\": \"181920\", \"secret\": \"secretKey\" }",
         |        "project_id": "731fc6f265cd486d900f16e84c5cb594",
         |        "type": "ec2",
         |        "user_id": "bb5476fd12884539b41d5a88f838d773"
         |    }
         |}""".stripMargin

    println(parser.parse(jsonString).flatMap(_.as[Response]))
  }
}

что в моем случае дало следующий результат:

Right(Response(Credentials(CredentialsBlob(181920,secretKey),731fc6f265cd486d900f16e84c5cb594,ec2,bb5476fd12884539b41d5a88f838d773)))

Для этого примера я использовал версию circe "0.12.3". Надеюсь это поможет!

person Ivan Kurchenko    schedule 05.02.2020
comment
Это хорошее улучшение, но основная проблема остается прежней. - person Simão Martins; 05.02.2020
comment
@SimãoMartins да, я согласен с тем, что для анализа этой строки до осмысленной структуры по-прежнему требуется ручная работа, но, с моей точки зрения, это не проблема, потому что это крайний случай, для которого была создана Decoder инфраструктура. Другим вариантом было бы изменить схему JSON, но, насколько я знаю, это внешний API, который не находится под вашим контролем. - person Ivan Kurchenko; 06.02.2020
comment
Вы говорите, что это невозможно сделать, используя только декодеры? Или другими словами, без использования модуля парсера? - person Simão Martins; 07.02.2020
comment
@SimãoMartins Боюсь, что да, потому что это крайний случай, не поддерживаемый библиотекой из коробки. Но я не думаю, что это действительно проблема, потому что, если вы считаете, что обычный JSON не должен присутствовать в виде строки в поле, следовательно, библиотека синтаксического анализа JSON в целом не будет поддерживать этот случай, а вместо этого будет инструмент для обработки этого случая , как пользовательский кодек в circe. В качестве доказательства я бы указал на Instant пример в circe doc - circe.github.io /circe/codecs/custom-codecs.html - person Ivan Kurchenko; 07.02.2020
comment
Если вы опубликуете ответ, в котором говорится, что это невозможно сделать без использования модуля парсера, я отмечу его как принятый/правильный (я не могу вспомнить терминологию) - person Simão Martins; 07.02.2020
comment
@SimãoMartins Не могли бы вы проголосовать или принять мой ответ, пожалуйста? - person Ivan Kurchenko; 12.02.2020