如何使用 Circe 解析动态 JSON

How to parse dynamic JSON with Circe

我正在尝试解析 JSON,其中同一字段可以是数组或对象。同样,特定字段可以是字符串或数字。请考虑以下示例。

  1. 空对象
{
 "technicalData": {}
}
  1. 字段为字符串或数字的集合
{
 "technicalData": [
   { 
      "techValueString": "0.173"
   },
   { 
      "techValueString": 0.173
   }
 ]
}

如何使用 Circe 映射到 Scala 类 在数据为 {} 时接受 Nil?

case class Response(technicalData: Seq[TechnicalData])

case class TechnicalData(techValueString: String)

谢谢。

这是解决您问题的一种非常冗长的方法,但我希望它具有让您识别甚至纠正您可能需要的每个极限情况的优点:

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))

我可能很快会推出一个较短的版本,但对极限情况的指示性较低。您可以使用您的好 json 的调整版本来探索它们。

希望对您有所帮助。

编辑:这是一个比上面更短更清晰的解决方案,它是在@Sergey Terentyev 很好地找到一个之后出现的。好吧,它可能以某种方式不太可读,但它倾向于用或多或少的方式处理极限情况来做同样的事情:

  // 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)) 

这是应用 Circe 的解码器的更简洁的解决方案

case class Response(technicalData: Seq[TechnicalData])

case class TechnicalData(techValueString: String)

class StringToResponse() extends (String => Response) {

  implicit val responseDecoder: Decoder[Response] = Decoder.instance { c =>
    for {
      technicalData <- c.downField("technicalData").focus match {
        case None => Right(Nil)
        case Some(seq) => seq.asArray match {
          case None => Right(Nil)
          case Some(_) => c.get[Seq[TechnicalData]]("technicalData")
        }
      }
    } yield {
      Response(technicalData)
    }
  }

  implicit val technicalDataDecoder: Decoder[TechnicalData] = (
    Decoder.instance(_.get[String]("techValueString")).or(
      Decoder.instance(_.get[Double]("techValueString").map(_.toString))
    )
  ) mapN TechnicalData

  override def apply(body: String): Response = {
    decode[Response](body) match {
      case Right(response) => response
      case Left(e) => throw new RuntimeException(e)
    }
  }
}

希望这对遇到类似问题的人有所帮助。