Search code examples
scalajsondecodercirce

Decode List[String] to List[JSONObject(key,value)] in circe scala


Given incoming json like below, how can i decode it the given case class based on condition.

Incoming JSON

{
    "config": {
        "files": ["welcome"],
        "channel": "media"
    }
}

Case Classes

case class File(`type`: String, value: String)

case class Config(files: List[File],
                  channel: String = "BC")

object Config{
  implicit val FileDecoder: Decoder[File] = deriveDecoder[File]
  implicit val ConfigDecoder: Decoder[Config] = deriveDecoder[Config]
}

case class Inventory(config: Config)

object Inventory {
  implicit val InventoryDecoder: Decoder[Inventory] = deriveDecoder[Inventory]
}

I do not have control on incoming files values in json it can be List[String] or List[File] so i need to handle both cases in my decoding logic.

So as we can see above my aim is to check if the incomes files values is List[String] then transform that values to below where type is hardcoded to "audio"

"files": [{
            "type": "audio",
            "value": "welcome.mp3"
        }],

The overall json should look like below before it mapped into case classes for auto decoding.

{
    "config": {
        "files": [{
            "type": "audio",
            "value": "welcome.mp3"
        }],
        "channel": "media"
    }
}

what i understood that this can be achieved either by transforming the json before decoding or can also be achieved during the decoding of files.

I tried writing the decoding logic at File level but i am not able to succeed. I am not getting the crux of how to do this.

Tried code

  implicit val FileDecoder: Decoder[File] = deriveDecoder[File].prepare { (aCursor: ACursor) =>
  {

    if(!aCursor.values.contains("type")){
      aCursor.values.map( v =>
        Json.arr(
          Json.fromFields(
            Seq(
              ("type", Json.fromString("audio")),
              ("value", v.head)
            )
          )
        )
      )
    }
  }
  }

Solution

  • We can use a custom Decoder for File to provide a default value to type

    final case class File(`type`: String, value: String)
    object File {
      implicit final val FileDecoder: Decoder[File] =
        Decoder.instance { cursor =>
          (
            cursor.getOrElse[String](k = "type")(fallback = "audio"),
            cursor.get[String](k = "value")
          ).mapN(File.apply)
        }.or(
          Decoder[String].map(value => File(`type` = "audio", value))
        )
    }
    

    Which can be used like this:

    val data =
    """[
      {
        "type": "audio",
        "value": "welcome.mp3"
      },
      {
        "value": "foo.mp3"
      },
      "bar.mp3"
    ]"""
    
    parser.decode[List[File]](data)
    // res: Either[io.circe.Error, List[File]] =
    //  Right(List(
    //    File("audio", "welcome.mp3"),
    //    File("audio", "foo.mp3"),
    //    File("audio", "bar.mp3")
    //  ))
    

    You can see the code running here.