How to parse json list or array in scala for play

2020-06-21 07:27发布

问题:

I am writing some RESTful API test cases and have little experience working with the scala playframwork.

Here is an example of my JSON.

  [ {
   "size" : "5082",
   "date-created" : "Wed Nov 19 17:10:39 CST 2014",
   "id" : "546d236fb84e894eefd8f769",
   "content-type" : "image/png",
   "filename" : "chrome-on-windows.PNG"
 }, {
   "size" : "15684",
   "date-created" : "Mon Jan 12 17:28:02 CST 2015",
   "id" : "54b4588266b3d11b1c1e9db6",
   "content-type" : "image/png",
   "filename" : "logos_ncsa.png"
 }, {
   "size" : "1267871",
   "date-created" : "Mon Jan 12 17:28:03 CST 2015",
   "id" : "54b4588366b3d11b1c1e9dba",
   "content-type" : "image/jpg",
   "filename" : "morrowplots.jpg"
 } ]

As you can see I have a list/Array of JSON items. I want to grab the id for the "morrowplots.jpg" file and store that into a variable to be used for successful API calls.

So I setup my code to look like the following. The result variable in the code below is the JSON string you see above.

  case class FileName(size: String, datecreated: String, id: String,    contenttype: String, filename: String)

  implicit val fileReads: Reads[FileName] = (
  (__ \\ "size").read[String] and
  (__ \\ "datecreated").read[String] and
  (__ \\ "id").read[String] and
  (__ \\ "content-type").read[String] and
  (__ \\ "filename").read[String]
  )(FileName.apply _)

  val json: JsValue = Json.parse(contentAsString(result))

  val nameResult: JsResult[FileName] = json.validate[FileName](fileReads)
  info("Right after validate")
    nameResult match {
      case s: JsSuccess[FileName] => {
        val testfile: FileName = s.get
        // Do something with testfile
        info("Success")
      }
      case e: JsError => {
        info("Error")
        info("Errors: " + JsError.toFlatJson(e).toString())
      }
    }

This gives me the following error.

[info] + Errors: {"objsize":[{"msg":"error.path.result.multiple","args":[]}],"objfilename":[{"msg":"error.path.resul t.multiple","args":[]}],"objid":[{"msg":"error.path.result.multiple","args":[]}],"objcontent-type":[{"msg":"error.path .result.multiple","args":[]}],"obj*datecreated":[{"msg":"error.path.missing","args":[]}]}

So how do I fix this List/Array issue and how do I search by filename to get the id?

Thanks in advance.

回答1:

I am no expert on Play, so this might not be idiomatic however it should solve your problem. First, your json is date-created versus your scala expecting datecreated. Second, you should only use one slash for your Reads. Next, you need to run validate against a List[FileName].

As to the filename search, you can now extract the list from the JsSuccess and run a filter against it.

The final code will look something like this

case class FileName(size: String, datecreated: String, id: String, contenttype: String, filename: String)

  implicit val fileReads: Reads[FileName] = (
  (__ \ "size").read[String] and
  (__ \ "date-created").read[String] and
  (__ \ "id").read[String] and
  (__ \ "content-type").read[String] and
  (__ \ "filename").read[String]
  )(FileName)

  val json: JsValue = Json.parse(contentAsString(result))

  val nameResult = json.validate[List[FileName]]

  val list = nameResult match {
      case JsSuccess(list : List[FileName], _) => list
      case e: JsError => {
        info("Errors: " + JsError.toFlatJson(e).toString())
        List()
      }
    }

  list filter(_.filename contains "morrow")


回答2:

First, you may decide to use the built-in json utilities rather than perform manual parsing.

  case class FileName(size: String, datecreated: String, id: String,    contenttype: String, filename: String)
  object FileName {
    implicit val formatFileName = Json.format[FileName]
  }

There, you have all you need to parse a basic json object. The compiler will use a macro (IIRC) to generate a code equivalent to the one you wrote by hand. As you don't have exotic validation on your fields, manual writing of the Reads and Writes classes are not necessary.

Then you can read it this way :

    def readString(str: String) = {
      val jsr: JsResult[Seq[FileName]] = Json.parse(str).validate[Seq[FileName]]
      jsr.fold(
        error => {
          ???
        },
        success => {
          ???
        }
      )
    }

jsr is here a JsResult. It can either be a JsSuccess or a JsError.

Note the complete type, too. As you have an array as your input, you should put your output un a collection, a Seq for instance.

You can fold on your JsResult. fold expects two functions. One is for the error case, it has type Seq[(JsPath, Seq[ValidationError])] => X, where X is the return type of your function. It shows you all the problems that have prevented your json to be translated to a Seq[FileName].

The other one is for the success case. It has type Seq[FileName] => X, with the same X than before.

You can now decide what to put in these two functions.

As pointed out by Justin, you can write it with a match too. It may be easier, even if less functional :

    def readString(str: String) = {
      val jsr: JsResult[Seq[FileName]] = Json.parse(str).validate[Seq[FileName]]
      jsr match {
        case JsResult(seq) => ???
        case e: JsError => ???
      }
    }