@tako_programingの忘備録とか

男子高校生の日常の忘備録

playのJSON Basicsをてきとうに和訳

Scalaの有名なウェブアプリケーションフレームワークにplayというものがあります。そのplayのJSONの扱いについて描かれているJSON Basicsをてきとうに和訳しました。


最近のウェブアプリケーションはよくJSONのデータをパースしたり作ったりする必要があります。PlayはJSONの扱いをJSON Libraryを経由してサポートしています。JSONとはは以下のような軽量なデータ・フォーマットです。

{
  "name" : "Watership Down",
  "location" : {
    "lat" : 51.235685,
    "long" : -1.309197
  },
  "residents" : [ {
    "name" : "Fiver",
    "age" : 4,
    "role" : null
  }, {
    "name" : "Bigwig",
    "age" : 6,
    "role" : "Owsla"
  } ]
}

JSON自体について更に詳しく知りたい場合はjson.orgを参照してください。

The Play JSON Libray

play.api.libs.jsonパッケージには、JSONデータを表すデータ構造と、そのデータ構造と他のデータ表現を変換するユーティリティが含まれています。これらはそのパッケージの機能の一部です。

  • Automatic conversion 最小の定型文を用いて、ケースクラスとの間で自動的な変換を行います。最小限のコードですぐに実行したい場合は、おそらくここから始めることになるでしょう。

  • Custom validation パース中のカスタムバリデーション

  • Automatic parsing リクエストボディでのJSONの自動解析で、コンテンツを解析できないまたは正常でないコンテンツタイプのヘッダが提供されたとき、エラーを自動生成します。

  • これらの機能はスタンドアローンのライブラリとしてplay以外でも使用することができます。そのためには、build.sbtlibraryDependencies + = "com.typesafe.play" %% "play-json"%playVersionを追加する必要があります。 このパッケージには、以下のようなタイプがあります。

    JsValue

    これはJSONの値を扱うトレイトです。このJSONライブラリは各JSONのタイプに対応するcase classが存在します。

  • JsString

  • JsNumber

  • JsBoolean

  • JsObject

  • JsArray

  • JsNull

これらのJsValueを利用することで、任意のJSON構造を作ることができます。

JSON

JSONオブジェクトは、主にJsValueとの変換用ユーティリティを提供します。

JsPath

これは、XMLXPathによく似たもので、JsValue構造へのパスを表します。これはJsValue構造をトラバースするために暗黙の変換のためのパターンで使用されます。

Converting to a JsValue

String型を使ってパースします。

import play.api.libs.json._

val json: JsValue = Json.parse("""
{
  "name" : "Watership Down",
  "location" : {
    "lat" : 51.235685,
    "long" : -1.309197
  },
  "residents" : [ {
    "name" : "Fiver",
    "age" : 4,
    "role" : null
  }, {
    "name" : "Bigwig",
    "age" : 6,
    "role" : "Owsla"
  } ]
}
""")

JsValueの各case classを用いてJSONを作成します。

val json: JsValue = JsObject(Seq(
  "name" -> JsString("Watership Down"),
  "location" -> JsObject(Seq("lat" -> JsNumber(51.235685), "long" -> JsNumber(-1.309197))),
  "residents" -> JsArray(Seq(
    JsObject(Seq(
      "name" -> JsString("Fiver"),
      "age" -> JsNumber(4),
      "role" -> JsNull
    )),
    JsObject(Seq(
      "name" -> JsString("Bigwig"),
      "age" -> JsNumber(6),
      "role" -> JsString("Owsla")
    ))
  ))
))

Json.objJson.arrを用いることで、JSONオブジェクトやJSON配列をもう少し簡潔に構築することができます。ほとんどの場合において、JsValueクラスで明示的にラップする必要がないことに注意しましょう。これらのファクトリメソッドは暗黙的に変換します。(詳細は後述)

val json: JsValue = Json.obj(
  "name" -> "Watership Down",
  "location" -> Json.obj("lat" -> 51.235685, "long" -> -1.309197),
  "residents" -> Json.arr(
    Json.obj(
      "name" -> "Fiver",
      "age" -> 4,
      "role" -> JsNull
    ),
    Json.obj(
      "name" -> "Bigwig",
      "age" -> 6,
      "role" -> "Owsla"
    )
  )
)

Writes convertersを使用する

ScalaからJsValueへの変換は、ユーティリティメソッドJson.toJson[T](T)(implicit writes: Writes[T])によって実行されます。このメソッドは、TはJsValueに変換するWrites[T]型のコンバータに依存します。 Play JSON APIは、Int, Double, String, Booleanなどのほとんど全てのプリミティブ型のimplicit Writesを提供します。また、Writes[T]が存在する任意の型TのコレクションのためのWritesをサポートします。

import play.api.libs.json._

// basic types
val jsonString = Json.toJson("Fiver")
val jsonNumber = Json.toJson(4)
val jsonBoolean = Json.toJson(false)

// collections of basic types
val jsonArrayOfInts = Json.toJson(Seq(1, 2, 3, 4))
val jsonArrayOfStrings = Json.toJson(List("Fiver", "Bigwig"))

独自のモデルをJsValueに変換するには、implicit Writesコンバータを定義し、それらをスコープで指定する必要があります。

case class Location(lat: Double, long: Double)
case class Resident(name: String, age: Int, role: Option[String])
case class Place(name: String, location: Location, residents: Seq[Resident])
import play.api.libs.json._

implicit val locationWrites = new Writes[Location] {
  def writes(location: Location) = Json.obj(
    "lat" -> location.lat,
    "long" -> location.long
  )
}

implicit val residentWrites = new Writes[Resident] {
  def writes(resident: Resident) = Json.obj(
    "name" -> resident.name,
    "age" -> resident.age,
    "role" -> resident.role
  )
}

implicit val placeWrites = new Writes[Place] {
  def writes(place: Place) = Json.obj(
    "name" -> place.name,
    "location" -> place.location,
    "residents" -> place.residents)
}

val place = Place(
  "Watership Down",
  Location(51.235685, -1.309197),
  Seq(
    Resident("Fiver", 4, None),
    Resident("Bigwig", 6, Some("Owsla"))
  )
)

val json = Json.toJson(place)

また、コンビネータパターンを用いてWritesを定義することも可能です。 注:コンビネータパターンについてはJSONReads/Writes/Formats Combinatorsで詳しく説明しています。

import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit val locationWrites: Writes[Location] = (
  (JsPath \ "lat").write[Double] and
  (JsPath \ "long").write[Double]
)(unlift(Location.unapply))

implicit val residentWrites: Writes[Resident] = (
  (JsPath \ "name").write[String] and
  (JsPath \ "age").write[Int] and
  (JsPath \ "role").writeNullable[String]
)(unlift(Resident.unapply))

implicit val placeWrites: Writes[Place] = (
  (JsPath \ "name").write[String] and
  (JsPath \ "location").write[Location] and
  (JsPath \ "residents").write[Seq[Resident]]
)(unlift(Place.unapply))

JSON Value構造をトラバースする

JsValue構造体をトラバースして特定の値を抽出することができます。その構文と機能はScalaXML処理ににています。 注:以下の例は、前の例で作成されたJsValue構造体に適用されます。

Simple path \

\演算子をJsValueに適用すると、これがJsObjectであると仮定して、フィールド引数に対応するプロパティを返します。

val lat = (json \ "location" \ "lat").get
// returns JsNumber(51.235685)

Recursive path \

\\演算子を適用すると、現在のオブジェクトとそのすべての子孫のフィールドが検知されます。

val names = json \\ "name"
// returns Seq(JsString("Watership Down"), JsString("Fiver"), JsString("Bigwig"))

Index lookup (for JsArrays)

インデックス番号の適用演算子を使用してJsArrayの値を取得できます。

val bigwig = (json \ "residents")(1)
// returns {"name":"Bigwig","age":6,"role":"Owsla"}

JsValueから変換する

文字列ユーティリティの使用 縮小版:

val minifiedString: String = Json.stringify(json)
{"name":"Watership Down","location":{"lat":51.235685,"long":-1.309197},"residents":[{"name":"Fiver","age":4,"role":null},{"name":"Bigwig","age":6,"role":"Owsla"}]}

可読版:

val readableString: String = Json.prettyPrint(json)
{
  "name" : "Watership Down",
  "location" : {
    "lat" : 51.235685,
    "long" : -1.309197
  },
  "residents" : [ {
    "name" : "Fiver",
    "age" : 4,
    "role" : null
  }, {
    "name" : "Bigwig",
    "age" : 6,
    "role" : "Owsla"
  } ]
}

バリデーションを使う

JsValueから他の型に変換するための好ましいのは、validateメソッド(引数はReads型)を使用する方法です。これにより、検証と変換の両方が実行され、JsResult型が返される。JsResultは次の2つのクラスで実装されています。

  • JsSuccess: 検証と変換が成功したことを表し、結果をラップします。

  • JsError: 検証と変換の失敗を表し、検証エラーのリストを含んでいます。 検証結果を処理するために様々なパターンを適用することができます。

val json = { ... }

val nameResult: JsResult[String] = (json \ "name").validate[String]

// Pattern matching
nameResult match {
  case s: JsSuccess[String] => println("Name: " + s.get)
  case e: JsError => println("Errors: " + JsError.toJson(e).toString())
}

// Fallback value
val nameOrFallback = nameResult.getOrElse("Undefined")

// map
val nameUpperResult: JsResult[String] = nameResult.map(_.toUpperCase())

// fold
val nameOption: Option[String] = nameResult.fold(
  invalid = {
    fieldErrors => fieldErrors.foreach(x => {
      println("field: " + x._1 + ", errors: " + x._2)
    })
    None
  },
  valid = {
    name => Some(name)
  }
)

JsValueから独自モデルへ

JsValueから独自のモデルに変換するには、implict Reads[T]を定義する必要があります。ここでは、Tはモデルのタイプである。 注:Readおよびカスタムバリデーションの実装に使用されるパターンについては、JSON Reads / Writes / Formats Combinatorsで詳しく説明しています。

case class Location(lat: Double, long: Double)
case class Resident(name: String, age: Int, role: Option[String])
case class Place(name: String, location: Location, residents: Seq[Resident])
import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit val locationReads: Reads[Location] = (
  (JsPath \ "lat").read[Double] and
  (JsPath \ "long").read[Double]
)(Location.apply _)

implicit val residentReads: Reads[Resident] = (
  (JsPath \ "name").read[String] and
  (JsPath \ "age").read[Int] and
  (JsPath \ "role").readNullable[String]
)(Resident.apply _)

implicit val placeReads: Reads[Place] = (
  (JsPath \ "name").read[String] and
  (JsPath \ "location").read[Location] and
  (JsPath \ "residents").read[Seq[Resident]]
)(Place.apply _)


val json = { ... }

val placeResult: JsResult[Place] = json.validate[Place]
// JsSuccess(Place(...),)

val residentResult: JsResult[Resident] = (json \ "residents")(1).validate[Resident]
// JsSuccess(Resident(Bigwig,6,Some(Owsla)),)

と、まあ自らの理解を深めるためにてきとうに和訳してみたので、明らかに意味が異なる場合なんかがあったら指摘してください。