Search code examples
jsonscalacircehttp4s-circecirce-optics

How to decode array containing json with Circe


I have my circe Decoder as shown below. I am confident my Sentiment Decoder works correctly so won't include it below.

case class CryptoData(value: String, valueClassification: Sentiment)
  implicit val decoder: Decoder[CryptoData] = Decoder.instance { json =>
    for {
      value               <- json.downField("data").get[String]("value")
      valueClassification <- json.downField("data").get[Sentiment]("value_classification")
    } yield CryptoData(value, valueClassification)
  }

my Json looks like this

{
  "name" : "Fear and Greed Index",
  "data" : [
    {
      "value" : "31",
      "value_classification" : "Fear",
      "timestamp" : "1631318400",
      "time_until_update" : "54330"
    }
  ],
  "metadata" : {
    "error" : null
  }
}

I simply want value and value_classification. As can be seen, those values sit within an array.

I suspect Circe is looking to decode a List[data] but I don't want to create a case class DataInfo(list: List[Data]) it just doesn't feel right.


Solution

  • You just missed a downArray call to parse data as the array of objects. Working decoder:

    implicit val cryptoDecoder: Decoder[CryptoData] = Decoder.instance { json =>
      val data = json.downField("data").downArray
      for {
        value <- data.get[String]("value")
        valueClassification <- data.get[Sentiment]("value_classification")
      } yield CryptoData(value, valueClassification)
    }
    

    Small recomendataion:

    I would advise you to define a basic decoder for CryptoData, it should just decode CryptoData from the data object:

    {
        "value" : "31",
        "value_classification" : "Fear",
        "timestamp" : "1631318400",
        "time_until_update" : "54330"
    }
    

    to:

    CryptoData("31", Fear)
    

    and if you have some extended JSON, you can just move down to the actual CryptoData field using some custom parser and parse object.

    full code:

    import io.circe
    import io.circe.Decoder
    import io.circe.parser._
    
    trait Sentiment
    
    object Sentiment {
      case object Fear extends Sentiment
    
      implicit val sentimentDecoder: Decoder[Sentiment] = Decoder.decodeString.map {
        case "Fear" => Fear
      }
    }
    
    case class CryptoData(value: String, valueClassification: Sentiment)
    
    object CryptoData {
      implicit val cryptoDecoder: Decoder[CryptoData] = Decoder.instance { json =>
        for {
          value <- json.downField("value").as[String]
          valueClassification <- json.downField("value_classification").as[Sentiment]
        } yield CryptoData(value, valueClassification)
      }
    
      def decodeRaw(extendedObject: String): Either[circe.Error, Array[CryptoData]] =
        parse(extendedObject).flatMap(json => json.hcursor.downField("data").as[Array[CryptoData]])
    }
    

    testing:

    val extendedJson =
      """
        |{
        |  "name" : "Fear and Greed Index",
        |  "data" : [
        |    {
        |      "value" : "31",
        |      "value_classification" : "Fear",
        |      "timestamp" : "1631318400",
        |      "time_until_update" : "54330"
        |    }
        |  ],
        |  "metadata" : {
        |    "error" : null
        |  }
        |}
        |""".stripMargin
    
    // here should be Array
    val result: Either[circe.Error, Array[CryptoData]] = CryptoData.decodeRaw(extendedJson)
    // Right(CryptoData(31,Fear))
    println(result.map(_.mkString(", ")))
    
    
    val cryptoDataJson =
      """
        |{
        |      "value" : "31",
        |      "value_classification" : "Fear",
        |      "timestamp" : "1631318400",
        |      "time_until_update" : "54330"
        |    }
        |""".stripMargin
    
    // Right(CryptoData(31,Fear))
    println(decode[CryptoData](cryptoDataJson))