3

I'm dealing with an API which expects a JSON object where one of the values (blob) is a JSON object stringified:

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

My domain class is:

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

Encoding the domain class is easy:

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
)

However decoding is much harder:

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)

I don't like this implementation because it forces me to depend on circe-parser, and to break the abstraction layer the Encoders/Decoders provide.

Is there a way to implement a Decoder which does double decoding in a general way?

Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
Simão Martins
  • 1,210
  • 11
  • 22

1 Answers1

1

Well, because described JSON is not really typical case, I'm not sure if it is possible to completely avoid manual parsing, but if you will change case class representing this structure, you can leverage some advantages, which circe offers. Please, find code example below:

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]))
  }
}

which in my case produced next result:

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

I used circe version "0.12.3" for this example. Hope this helps!

Ivan Kurchenko
  • 4,043
  • 1
  • 11
  • 28
  • That is a nice improvement but the fundamental problem remains the same. – Simão Martins Feb 05 '20 at 16:30
  • @SimãoMartins yes, I agree that this still requires manual work to parse this string to meaningful structure, but from my point of view this is not really a problem, because it is corner case, for which `Decoder` infrastructure was made. Another option would be to change JSON schema, but AFAIK it is external API which is not under your control. – Ivan Kurchenko Feb 06 '20 at 16:54
  • Are you saying its impossible to do using only Decoders? Or in other words, without using the parser module? – Simão Martins Feb 06 '20 at 23:48
  • @SimãoMartins I'm afraid so, because this is corner case, not supported by the lib out of the box. But, I don't think this is really a problem, because if you think ussual JSON should not be present as a string in field, hence JSON parsing lib in general won't support this case, but instead will instrument to handle this case, like custom codec in circe. As a prove I would point to `Instant` example in circe doc - https://circe.github.io/circe/codecs/custom-codecs.html – Ivan Kurchenko Feb 07 '20 at 08:01
  • 1
    If you post an answer saying its impossible to do without using the parser module I'll mark it as accepted/correct (I cant remember the terminology) – Simão Martins Feb 07 '20 at 14:28
  • @SimãoMartins Could you upvote or accept my answer, please? – Ivan Kurchenko Feb 12 '20 at 08:57