Как проанализировать динамический JSON с Circe - PullRequest
1 голос
/ 02 мая 2019

Я пытаюсь проанализировать 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)

Спасибо.

Ответы [ 2 ]

1 голос
/ 05 мая 2019

Вот менее подробное решение с применением декодеров Цирцеи

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

Надеюсь, это поможет кому-то, кто столкнется с подобной проблемой.

1 голос
/ 03 мая 2019

Это действительно многословный способ решения вашей проблемы, но я надеюсь, что у него есть преимущество, позволяющее вам идентифицировать или даже исправить все предельные случаи, которые могут вам понадобиться:

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