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?

1 Answer 1

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!

Sign up to request clarification or add additional context in comments.

6 Comments

That is a nice improvement but the fundamental problem remains the same.
@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.
Are you saying its impossible to do using only Decoders? Or in other words, without using the parser module?
@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 - circe.github.io/circe/codecs/custom-codecs.html
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)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.