Search code examples
jsonscalascala-catscirce

couldn't decode Object with field Type Map[String, String] using circe


I have an case class which contains field of type Map[String, String]

The complete class definition is -

case class MyConfig(version: Int, pairs: Map[String, String]).

The json I'm trying to decode is -

{  
   "version":1,
   "pairs":[  
      {  
         "key1":"value1",
         "key2":"value2"
      }
   ]
}

When I try to decode the string into MyConfig object println(decode[MyConfig](jsonStr)) I get the below error -

Left(DecodingFailure([K, V]Map[K, V], List(DownField(pairs)))).

The complete code is -

case class MyConfig(version: Int, pairs: Map[String, String])

  import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._

  val jsonStr = """    {
                  |       "version":1,
                  |       "pairs":[
                  |          {
                  |             "key1":"value1",
                  |             "key2":"value2"
                  |          }
                  |       ]
                  |    }  """.stripMargin

  println(jsonStr)

  println(decode[MyConfig](jsonStr))

I'm able to decode Map json object as demonstrated here but not object with map field.

Any idea how to resolve this error?


Solution

  • The problem is that the generically derived decoder will try to parse the "pairs" value as a Map[String, String], which means it will look for a JSON object, while what you have is a JSON array of objects.

    If you're stuck with that MyConfig definition and input that's shaped like that, you're probably best off writing your own decoder instead of deriving one with io.circe.generic.auto. This is pretty straightforward with forProductN:

    case class MyConfig(version: Int, pairs: Map[String, String])
    
    import io.circe.Decoder
    
    implicit val decodeMyConfig: Decoder[MyConfig] =
      Decoder.forProduct2[MyConfig, Int, List[Map[String, String]]](
        "version",
        "pairs"
      ) {
        case (v, ps) => MyConfig(v, ps.flatten.toMap)
      }
    

    And then, assuming you've defined jsonStr as above:

    scala> import io.circe.parser.decode
    import io.circe.parser.decode
    
    scala> decode[MyConfig](jsonStr)
    res0: Either[io.circe.Error,MyConfig] = Right(MyConfig(1,Map(key1 -> value1, key2 -> value2)))
    

    Alternatively you could either change MyConfig so that the pairs member is a List[Map[String, String]], or you could change the JSON schema (or whatever code is generating it) to omit the JSON array layer.