3
votes

Suppose I have such json

{
  "sha": "some sha",
  "parents": [{
    "url": "some url",
    "sha": "some parent sha"
  }]
}

and such case class

case class Commit(sha: String, parentShas: List[String])

In play-json I could write the reads like this:

val commitReads: Reads[Commit] = (
  (JsPath \ "sha").read[String] and
  (JsPath \ "parents" \\ "sha").read[List[String]]
)(Commit.apply _)

I'm looking for a equivalent way of decoding only the "sha" of "parent" in argonaut/circe but I haven't found any. "HCursor/ACursor" has downArray but from there on I don't know what to do. Thank you very much in advance!

1

1 Answers

3
votes

Neither circe nor Argonaut keeps track of which fields have been read in JSON objects, so you can just ignore the extra "url" field (just as in Play). The trickier part is finding the equivalent of Play's \\, which circe doesn't have at the moment, although you've convinced me we need to add it.

First of all, this is relatively easy if you have a separate SHA type:

import io.circe.Decoder

val doc = """
{
  "sha": "some sha",
  "parents": [{
    "url": "some url",
    "sha": "some parent sha"
  }]
}
"""

case class Sha(value: String)

object Sha {
  implicit val decodeSha: Decoder[Sha] = Decoder.instance(_.get[String]("sha")).map(Sha(_))
}

case class Commit(sha: Sha, parentShas: List[Sha])

object Commit {
  implicit val decodeCommit: Decoder[Commit] = for {
    sha <- Decoder[Sha]
    parents <- Decoder.instance(_.get[List[Sha]]("parents"))
  } yield Commit(sha, parents)
}

Or, using Cats's applicative syntax:

import cats.syntax.cartesian._

implicit val decodeCommit: Decoder[Commit] = 
  (Decoder[Sha] |@| Decoder.instance(_.get[List[Sha]]("parents"))).map(Commit(_, _))

And then:

scala> import io.circe.jawn._
import io.circe.jawn._

scala> decode[Commit](doc)
res0: cats.data.Xor[io.circe.Error,Commit] = Right(Commit(Sha(some sha),List(Sha(some parent sha))))

But that's not really an answer, since I'm not going to ask you to change your model. :) The actual answer is a bit less fun:

case class Commit(sha: String, parentShas: List[String])

object Commit {
  val extractSha: Decoder[String] = Decoder.instance(_.get[String]("sha"))

  implicit val decodeCommit: Decoder[Commit] = for {
    sha <- extractSha
    parents <- Decoder.instance(c =>
      c.get("parents")(Decoder.decodeCanBuildFrom[String, List](extractSha, implicitly))
    )
  } yield Commit(sha, parents)
}

This is bad, and I'm ashamed it's necessary, but it works. I've just filed an issue to make sure this gets better in a future circe release.