2
votes

How can I write circe decoder for class

case class KeyValueRow(count: Int, key: String)

where json contains field "count" (Int) and some extra string-field (name of this field may be various, like "url", "city", whatever)?

{"count":974989,"url":"http://google.com"}
{"count":1234,"city":"Rome"}
1
Your example json contains three bits of data: the count, the name of the key for the string, and the value for the string. Your data class only has two fields - is your intent to throw away the value? - Gareth Latty
I need instances KeyValueRow(974989, "google.com") and KeyValueRow(1234, "Rome"). Some external service returns a bunch of jsons in format {"count":numType,"???":stringType}. I don't want create a lot of case-classes. - Pavel Ajtkulov
If you do this, not keeping track of what the name of the field in the decoded string was, you will not be able to encode back. That is: encode(decode("""{"count":974989,"url":"google.com""""})) != """{"count":974989,"url":"google.com"}""". Is that acceptable? - mdm
@mdm, I need decoder only. Thanks a lot - Pavel Ajtkulov

1 Answers

2
votes

You can do what you need like this:

import io.circe.syntax._
import io.circe.parser._
import io.circe.generic.semiauto._
import io.circe.{ Decoder, Encoder, HCursor, Json, DecodingFailure}

object stuff{
  case class KeyValueRow(count: Int, key: String)

  implicit def jsonEncoder : Encoder[KeyValueRow] = deriveEncoder

  implicit def jsonDecoder : Decoder[KeyValueRow] = Decoder.instance{ h =>
    (for{
      keys <- h.keys
      key <- keys.dropWhile(_ == "count").headOption
    } yield { 
      for{
        count <- h.get[Int]("count")
        keyValue <- h.get[String](key)
      } yield KeyValueRow(count.toInt, keyValue)
    }).getOrElse(Left(DecodingFailure("Not a valid KeyValueRow", List())))
  }
}

import stuff._

val a = KeyValueRow(974989, "www.google.com")

println(a.asJson.spaces2)

val test1 = """{"count":974989,"url":"http://google.com"}"""
val test2 = """{"count":1234,"city":"Rome", "will be dropped": "who cares"}"""

val parsedTest1 = parse(test1).flatMap(_.as[KeyValueRow])
val parsedTest2 = parse(test2).flatMap(_.as[KeyValueRow])

println(parsedTest1)
println(parsedTest2)

println(parsedTest1.map(_.asJson.spaces2))
println(parsedTest2.map(_.asJson.spaces2))

Scalafiddle: link here

As I mentioned in the comment above, keep in mind that the that if you decode some json, and then re-encode it, the result will be different from the initial input. To fix that, you would need to keep track of the original name of the key field.