§Play JSON ライブラリの基礎
§概要
JSON を使いたければ、play.api.libs.json
にある JSON ライブラリを基にした Play の型クラスを使うことをおすすめします。
JSON 文字列をパースするために、Play は超高速な Java ベースの JSON ライブラリ、Jerkson を使います。
このアプローチの利点として、Scala ユーザーは Play の JSON サポートがお膳立てする特上の型安全性と関数型の側面を楽しむことができる一方、Java と Scala の両方の Play は同じ基礎となるライブラリ (Jackson) を共有することができます。
§JSON は AST (_Abstract Syntax Tree: 抽象構文木_)
JSON の例を見てください:
{
"user": {
"name" : "toto",
"age" : 25,
"email" : "[email protected]",
"isAlive" : true,
"friend" : {
"name" : "tata",
"age" : 20,
"email" : "[email protected]"
}
}
}
これは、以下の二つの仕組みを使った木構造として見ることができます:
- JSON オブジェクト には
name
/value
のペアのセットが含まれます:name
は文字列value
は以下のいずれかです :- 文字列
- 数値
- 他の JSON オブジェクト
- JSON 配列
- true/false
- null
- JSON 配列 は、上述した value の型からなる値の列です。
正確な JSON 標準について詳しく知りたい場合は、json.org へアクセスしてください
§Json データ型
play.api.libs.json
には、前述の構造を厳密に反映した 7 つの JSON データ型が含まれています。
§JsObject
- 標準で説明されている name/value ペアのセットです。
- 例えば
{ "name" : "toto", "age" : 45 }
は JSON です。
§JsNull
JSON の null
を表現します
§JsBoolean
true
か false
の値を取る真偽値です
§JsNumber
- JSON は
short
,int
,long
,float
,double
,bigdecimal
を区別しないので、bigdecimal
を含むJsNumber
として表現されます。 - Play の JSON API は、より正確な型の Scala オブジェクトに変換します。
§JsArray
- 配列は、あらゆる Json 値の型の列です (同じ型である必要はありません)
- 例えば
[ "alpha", "beta", true, 123.44, 334]
は JSON です。
§JsString
標準的な文字列です。
§JsUndefined
これは JSON 標準の一部ではなく、抽象構文木においてエラーとなったノードを表現するために API が内部でのみ使用します。
§JsValue
上記のすべての型は、総称的な JSON トレイトである JsValue
を継承します。
§基本的な JSON API を使うための最小 import
import play.api.libs.json.Json
この import は、もっとも基本的な JSON 機能へのアクセスを提供します:
Json.parse
: 文字列を JsValue にパースしますJson.stringify
: JsValue をコンパクトな文字列にします (改行、インデントなし)Json.prettyPrint
: JsValue を整形された文字列にします (改行、インデントあり)Json.toJson[T](t: T)(implicit writes: Writes[T])
: 解決された暗黙のWrites[T]
を使った Scala オブジェクトからJsValue
への変換を試みますJson.fromJson[T](json: JsValue)(implicit reads: Reads[T])
: 解決された暗黙のReads[T]
を使ったJsValue
から Scala オブジェクトへの変換を試みますJson.obj()
:JsObject
を作成するための簡素化された文法ですJson.arr()
:JsArray
を作成するための簡素化された文法です
§JSON 文字列のパース
どのような JSON 文字列でも JsValue
として簡単にパースすることができます:
import play.api.libs.json.Json
val json: JsValue = Json.parse("""
{
"user": {
"name" : "toto",
"age" : 25,
"email" : "[email protected]",
"isAlive" : true,
"friend" : {
"name" : "tata",
"age" : 20,
"email" : "[email protected]"
}
}
}
""")
このサンプルは、以下すべての例で使用します。
上記にて説明した通り、パースは Jackson によって行われます。
§JSON ディレクトリの構築
§無骨な方法
上記した Json オブジェクトの例は、別の方法でも作成することができます。
以下は無骨なアプローチです。
import play.api.libs.json._
JsObject(
"users" -> JsArray(
JsObject(
"name" -> JsString("Bob") ::
"age" -> JsNumber(31) ::
"email" -> JsString("[email protected]") ::
Nil) ::
JsObject(
"name" -> JsString("Kiki") ::
"age" -> JsNumber(25) ::
"email" -> JsNull ::
Nil
) :: Nil
) :: Nil
)
§推奨する方法
Play は JSON を作成する簡素化された文法を提供しています。
上記の JsObject は以下のようにして構築することができます:
import play.api.libs.json.Json
Json.obj(
"users" -> Json.arr(
Json.obj(
"name" -> "bob",
"age" -> 31,
"email" -> "[email protected]"
),
Json.obj(
"name" -> "kiki",
"age" -> 25,
"email" -> JsNull
)
)
)
§JSON のシリアライズ
JsValue
を JSON 文字列表現にシリアライズするのは簡単です:
import play.api.libs.json.Json
val jsonString: String = Json.stringify(jsValue)
§JSON ツリー内パスへのアクセス
JsValue
さえ手に入れば、JSON ツリーの中を探索することができます。
この API は、JsValue
を探索することを除けば、Scala で NodeSeq
を使って XML ドキュメントを探索するために提供されている API と似ています。
§シンプルな \
パス
// 様々な種類の Json を操作する必要がある場合に備えて、ここでは json パッケージ配下をすべてインポートします
scala> import play.api.libs.json._
scala> val name: JsValue = json \ "user" \ "name"
name: play.api.libs.json.JsValue = "toto"
§再帰的な \\
パス
// サブツリーを再帰的に検索し、見つかったすべての JsValue に対する
// Seq[JsValue] を返します
scala> val emails: Seq[String] = json \ "user" \\ "email"
emails: Seq[play.api.libs.json.JsValue] = List("[email protected]", "[email protected]")
§JsValue から Scala 値への変換
JSON ツリーを渡り歩いていると JsValue
を見つけることができますが、JsValue から Scala の型へ変換したくなるかもしれません。
例えば、JsString
を String
に、または (もし変換できるなら) JsNumber
を Long
に、と言った具合です。
§json.as[T]
による安全でない変換
as[T]
は、パスにアクセスして要求された型に変換しようとするので、安全ではありません。しかし、パスが見つからなかったり、変換が不可能だった場合は、検出されたエラーを含む実行時例外 JsResultException
を生成します。
§成功した場合: パスが見つかり、変換できる
// (変換が可能でパスが見つかった場合) 提供された型に変換された値を返します
scala> val name: String = (json \ "user" \ "name").as[String]
name: String = toto
§失敗した場合: パスが見つからない
scala> val nameXXX: String = (json \ "user" \ "nameXXX").as[String]
play.api.libs.json.JsResultException: JsResultException(errors:List((,List(ValidationError(validate.error.expected.jsstring,WrappedArray())))))
at play.api.libs.json.JsValue$$anonfun$4.apply(JsValue.scala:65)
at play.api.libs.json.JsValue$$anonfun$4.apply(JsValue.scala:65)
at play.api.libs.json.JsResult$class.fold(JsResult.scala:69)
at play.api.libs.json.JsError.fold(JsResult.scala:10)
at play.api.libs.json.JsValue$class.as(JsValue.scala:63)
at play.api.libs.json.JsUndefined.as(JsValue.scala:96)
このエラーは、期待しているであろう
path.not.found
を返さないことに注意してください。これは、このドキュメントの後の方で登場する JSON コンビネーターとは異なります。
これは、(json \ "user" \ "nameXXX")
がJsNull
を返却し、暗黙のReads[String]
は検出されたエラーで説明されている通りJsString
を待ち受けていることによるものです。
§失敗した場合: 変換できない
scala> val name: Long = (json \ "user" \ "name").as[Long]
play.api.libs.json.JsResultException: JsResultException(errors:List((,List(ValidationError(validate.error.expected.jsnumber,WrappedArray())))))
at play.api.libs.json.JsValue$$anonfun$4.apply(JsValue.scala:65)
at play.api.libs.json.JsValue$$anonfun$4.apply(JsValue.scala:65)
at play.api.libs.json.JsResult$class.fold(JsResult.scala:69)
at play.api.libs.json.JsError.fold(JsResult.scala:10)
at play.api.libs.json.JsValue$class.as(JsValue.scala:63)
at play.api.libs.json.JsString.as(JsValue.scala:111)
§Option[T]
による、より安全な変換
as[T]
は即時性はあるものの堅牢ではないので、あらゆる型のエラーが発生した場合に None を返す asOpt[T]
が用意されています。
§成功した場合: パスが見つかり、変換できる
scala> val maybeName: Option[String] = (json \ "user" \ "name").asOpt[String]
maybeName: Option[String] = Some(toto)
§失敗した場合: パスが見つからない
scala> val maybeNameXXX: Option[String] = (json \ "user" \ "nameXXX").asOpt[String]
maybeNameXXX: Option[String] = None
§失敗した場合: 変換できない
scala> val maybeNameLong: Option[Long] = (json \ "user" \ "name").asOpt[Long]
maybeNameLong: Option[Long] = None
§validate[T]
による、もっとも安全な変換
asOpt[T]
は、より安全ですが、検出されたエラー等を失ってしまいます。
validate[T]
は、JsResult[T]
を返すことで、JsValue
をもっとも安全で堅牢な方法を提供します:
JsResult[T]
は (最初のエラーに留まらずに) 検出されたすべてのエラーを蓄積します。JsResult[T]
は、これを操作、構成するmap
/flatMap
/fold
を提供するモナド構造です。
§JsResult[T]
をひと言で
JsResult[T]
は二つの値を持つことができます:
- 以下を含む
JsSuccess[T](value: T, path: JsPath = JsPath())
:- 変換に成功した場合には
value: T
- ちなみに、
path
は走査された最新のJsPath
を表現するために API が使う内部的なフィールドなので、気にしないでください。
- 変換に成功した場合には
注意 :
JsPath
については後ほど説明しますが、JSON におけるXMLPath
とまったく同じものです。json \ "user" \ "name"
と書く場合、以下のように書くことができます :(JsPath \ "user" \ "name")(json)
user
に続けてname
を検索するJsPath
を作成し、これを与えられたjson
に適用します。
JsError(errors: Seq[(JsPath, Seq[ValidationError])])
:errors
は(JsPath, Seq[ValidationError])
ペアの列です(JsPath, Seq[ValidationError])
ペアは、与えられたJsPath
で検出したひとつ以上のエラーを示します
いくつかの使用例:
scala> import play.api.libs.json._
scala> val jsres: JsResult[String] = JsString("toto").validate[String]
jsres: JsSuccess("toto")
scala> val jsres: JsResult[String] = JsNumber(123).validate[String]
jsres: play.api.libs.json.JsResult[String] = JsError(List((,List(ValidationError(validate.error.expected.jsstring,WrappedArray())))))
jsres.map{ s: String => …}
jsres.flatMap{ s: String => JsSuccess(s) }
jsres.fold(
errors: Seq[(JsPath, Seq[ValidationError])] => // エラーを処理して,
s: String => // 値を処理する
)
jsres.map( s: String => // manage value )
.recoverTotal( jserror: JsError => // エラーを処理をしてデフォルト値を返す)
§成功した場合: パスが見つかり、変換できる
scala> val safeName = (json \ "user" \ "name").validate[String]
safeName: play.api.libs.json.JsResult[String] = JsSuccess(toto,) // path は root なので厳密ではありません
§失敗した場合: パスが見つからない
scala> val nameXXX = (json \ "user" \ "nameXXX").validate[String]
nameXXX: play.api.libs.json.JsResult[String] =
JsError(List((,List(ValidationError(validate.error.expected.jsstring,WrappedArray())))))
このエラーは、期待しているであろう
path.not.found
を返さないことに注意してください。これは、このドキュメントの後の方で登場する JSON コンビネーターとは異なります。
これは、(json \ "user" \ "nameXXX")
がJsNull
を返却し、暗黙のReads[String]
は検出されたエラーで説明されている通りJsString
を待ち受けていることによるものです。
§失敗した場合: 変換できない
scala> val name = (json \ "user" \ "name").validate[Long]
name: play.api.libs.json.JsResult[Long] =
JsError(List((,List(ValidationError(validate.error.expected.jsnumber,WrappedArray())))))
§再帰的なパス \\
の変換
\\
はサブツリーを再帰的に検索し、標準的な Scala 関数のコレクションである、発見した JsValue の Seq[JsValue]
を返します。
scala> val emails: Seq[String] = (json \ "user" \\ "email").map(_.as[String])
emails: Seq[String] = List([email protected], [email protected])
§Scala 値から JsValue への変換
Scala から JSON への変換は、ちょうど T
から JsValue
に変換するために利用できる、暗黙的な型クラス Writes[T]
に基づいた Json.toJson[T](implicit writes: Writes[T])
関数によって行われます。
§とてもシンプルな JsValue を作る
val jsonNumber = Json.toJson(4)
jsonNumber: play.api.libs.json.JsValue = 4
Play の JSON API が暗黙の Writes[Int]
を提供するので、このように変換することができます
§Seq[T] から JSON 配列を作る
import play.api.libs.json.Json
val jsonArray = Json.toJson(Seq(1, 2, 3, 4))
jsonArray: play.api.libs.json.JsValue = [1,2,3,4]
Play の JSON API が暗黙の Writes[Seq[Int]]
を提供するので、このように変換することができます
ここで、Seq[Int]
は問題なく Json 配列に変換することができました。しかし、 Seq
が異質な値を含むとすると、より複雑です:
import play.api.libs.json.Json
val jsonArray = Json.toJson(Seq(1, "Bob", 3, 4))
<console>:11: error: No Json deserializer found for type Seq[Any]. Try to implement an implicit Writes or Format for this type.
val jsonArray = Json.toJson(Seq(1, "Bob", 3, 4))
Seq[Any]
を Json に変換する手段がないためエラーになります (Any
は Json でサポートされていないものまで含むことができますよね?)
シンプルな解決方法は、これを Seq[JsValue]
として扱うことです:
import play.api.libs.json.Json
val jsonArray = Json.toJson(Seq(
toJson(1), toJson("Bob"), toJson(3), toJson(4)
))
Play の JSON API が暗黙の Writes[Seq[JsValue]]
を提供するので、このように変換することができます
§Map[String, T] から JSON オブジェクトを作る
import play.api.libs.json.Json
val jsonObject = Json.toJson(
Map(
"users" -> Seq(
toJson(
Map(
"name" -> toJson("Bob"),
"age" -> toJson(31),
"email" -> toJson("[email protected]")
)
),
toJson(
Map(
"name" -> toJson("Kiki"),
"age" -> toJson(25),
"email" -> JsNull
)
)
)
)
)
これは次の Json を生成します:
{
"users":[
{
"name": "Bob",
"age": 31.0,
"email": "[email protected]"
},
{
"name": "Kiki",
"age": 25.0,
"email": null
}
]
}
Next: Json の読み/書き/フォーマットの結合