Search code examples
jsonscalacirce

Custom circe decoder for variant json-field


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"}

Solution

  • 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.