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.
{
"technicalData": {}
}
{
"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.
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))