2

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"}
Pavel Ajtkulov
  • 515
  • 7
  • 21
  • 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 Apr 05 '18 at 23:57
  • I need instances KeyValueRow(974989, "http://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 Apr 06 '18 at 07:34
  • 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":"http://google.com""""})) != """{"count":974989,"url":"http://google.com"}""". Is that acceptable? – mdm Apr 06 '18 at 09:26
  • @mdm, I need decoder only. Thanks a lot – Pavel Ajtkulov Apr 06 '18 at 11:09

1 Answers1

3

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.

mdm
  • 3,928
  • 3
  • 27
  • 43