Search code examples
scalacirce

How to parse dynamic JSON with Circe


I'm trying to parse JSON where same field can be either array or object. Same as, specific field can be either string or number. Please consider examples below.

  1. Empty object
{
 "technicalData": {}
}
  1. Collection with field being either string or number
{
 "technicalData": [
   { 
      "techValueString": "0.173"
   },
   { 
      "techValueString": 0.173
   }
 ]
}

How can I do it with Circe mapping to Scala classes accepting Nil when data is {}?

case class Response(technicalData: Seq[TechnicalData])

case class TechnicalData(techValueString: String)

Thanks.


Solution

  • This is a really verbose way of resolving your problem but I hope it has the advantage of letting you identify, or even rectify, every limit cases, which you might need:

    import io.circe._
    import io.circe.parser.parse
    
    case class Response(technicalData: Seq[TechnicalData])
    
    case class TechnicalData(techValueString: String)
    
    val stringAsJson1 = """{
    
     "technicalData": {}
    }"""
    
    val stringAsJson2 = """{
     "technicalData": [
       { 
          "techValueString": "0.173"
       },
       { 
          "techValueString": 0.173
       }
     ]
    }"""
    
    
    def manageTechnicalDataAsArray(jsonArray: Vector[io.circe.Json]): Response = {
        Response(
          jsonArray.map(cell => {
            val value = cell.asObject
                            .getOrElse(throw new Exception("technicalData as a array should have each cell as an object"))
                            .apply("techValueString")
                            .getOrElse(throw new Exception("techValueString should be a key of any cell under technicalData array"))
            TechnicalData(value.asNumber
                               .map(_.toString)
                               .getOrElse(
                                value.asString
                                     .getOrElse(throw new Exception("techValueString value should be either string or number"))
                               )
                         )
                         }
                   )
                 )
    }
    
    def manageTechnicalDataAsObject(jsonObject: io.circe.JsonObject): Response = {
        jsonObject.toIterable match {
             case empty if empty.isEmpty => Response(Nil)
             case _ => throw new Exception("technicalData when object should be empty")
        }
    }
    
    def parseResponse(jsonAsString: String): Response = {
        parse(jsonAsString).getOrElse(Json.Null)
                           .asObject
                           .map(_("technicalData")
                                 .getOrElse(throw new Exception("the json should contain a technicalData key"))
                                 .arrayOrObject(throw new Exception("technicalData should contain either an objet or array"),
                                                manageTechnicalDataAsArray,
                                                manageTechnicalDataAsObject
                                 )
                           ).getOrElse(throw new Exception("the json should contain an object at top"))
    }
    
    println(parseResponse(stringAsJson1))
    println(parseResponse(stringAsJson2))
    

    I might come with a shorter version soon but less indicative on limit cases. You can explore them with tweaked version of a good json of yours.

    Hope it helps.

    EDIT: Here is a shorter and cleaner solution than above, which come after @Sergey Terentyev well found one. Well, it might be less readeable somehow, but it tends to do the same thing with more or less way to handle limit cases:

      // Structure part
      case class TechnicalData(techValueString: String)
      object TechnicalData {
        def apply[T](input: T) = new TechnicalData(input.toString)
      }
    
      case class Response(technicalData: Seq[TechnicalData])
    
      // Decoding part
      import io.circe.{Decoder, parser, JsonObject, JsonNumber}
      import io.circe.Decoder.{decodeString, decodeJsonNumber}
    
      def tDDGenerator[C : Decoder]: Decoder[TechnicalData] = Decoder.forProduct1("techValueString")(TechnicalData.apply[C])
    
      implicit val technicalDataDecoder: Decoder[TechnicalData] = tDDGenerator[String].or(tDDGenerator[JsonNumber])
    
      implicit val responseDecoder: Decoder[Response] = Decoder[JsonObject]
        .emap(_("technicalData").map(o => Right(o.as[Seq[TechnicalData]].fold(_ => Nil, identity)))
          .getOrElse(Right(Nil))
          .map(Response.apply))
    
      // Test part
    
      val inputStrings = Seq(
        """{
          | "technicalData": [
          |   {
          |      "techValueString": "0.173"
          |   },
          |   {
          |      "techValueString": 0.173
          |   }
          | ]
          |}
      """.stripMargin,
        """{
          | "technicalData": {}
          |}
      """.stripMargin
      )
    
      inputStrings.foreach(parser.decode[Response](_).fold(println,println))