play-json の使い方
Play Framework では json を扱うためのライブラリ play-json が提供されています。 扱いやすいライブラリだと思うのですがいつも細かい API を忘れてしまうので整理しておきたいと思いました。
ここで使用しているコードは こちら のリポジトリに保管しています。
1. Json を表現する型
play-json では json を JsValue
型で表現しており、そのデータ型は Haskell 風に書けば以下のようになります。
data JsValue = JsObject (Map String JsValue)
| JsArray [JsValue]
| JsNumber BigDecimal
| JsString String
| JsBoolean Boolean
| JsNull
2. 文字列との単純な相互変換
文字列と JsValue
との変換は それぞれ Json.parse
, Json.stringify
を使用できます。
val rawString =
"""
|{
| "name": "Alice",
| "age": 20,
| "favorites": ["tennis", "swimming"],
| "family": [
| {
| "name": "Bob",
| "relation": "father"
| },
| {
| "name": "Catharin",
| "relation": "mother"
| }
| ]
|}
""".stripMargin
// String to JsValue
// `Json.parse` throws exception for invalid input
val json: JsValue = Json.parse(rawString)
println(s"parsed json: $json")
// JsValue to String
// `Json.prettyPrint` is also available to print pretty json
println(s"reversed to string: ${Json.stringify(json)}")
3. Json に対する操作
Json 上のデータを簡単に取得するには \
, \\
を使用します。
// Traverse json by `\`
// JsLookupResult is like
// data JsLookupResult = JsDefined JsValue
// | JsUndefined String
val lookupName: JsLookupResult = json \ "name"
println(lookupName.get) // "Alice"
val lookupUnknown = json \ "xxx"
lookupUnknown match {
case JsDefined(value) =>
println(value)
case err @ JsUndefined() =>
println(err) // JsUndefined('xxx'List([{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}]) is undefined on object: {"name":"Alice","age":20,"favorites":["tennis","swimming"],"family":[{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}]})
}
// `\` can be repeated
println(json \ "favorites" \ 1) // JsDefined("swimming")
println(json \ "xxx" \ "yyy") // JsUndefined('xxx' is undefined on object: {"name":"Alice","age":20,"favorites":["tennis","swimming"],"family":[{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}]})
// Retrieve values by `\\`
val selectFamily: Seq[JsValue] = json \\ "family"
println(selectFamily) // List([{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}])
println(json \\ "xxx") // List()
4. Reads, Writes, Format 型クラス
実際のプログラミングでは与えられた json をそのまま json として扱うより適切なオブジェクトへと変換して扱い、また必要ならば json へと変換したいという場合が多いと思います。
そうした場合 play-json で用意されている Reads
, Writes
型クラスを使用します。
Reads 型クラス
あるクラスを Reads
にするためには reads(json: JsValue): JsResult[A]
を実装する必要があります (他のものはデフォルト実装があり、ここでは省略している)。
trait Reads[A] { self =>
/**
* Convert the JsValue into a A
*/
def reads(json: JsValue): JsResult[A]
def map[B](f: A => B): Reads[B] = ...
def flatMap[B](f: A => Reads[B]): Reads[B] = ...
def filter(f: A => Boolean): Reads[A] = ...
def filter(error: JsonValidationError)(f: A => Boolean): Reads[A] = ...
def filterNot(f: A => Boolean): Reads[A] = ...
def filterNot(error: JsonValidationError)(f: A => Boolean): Reads[A] = ...
def collect[B](error: JsonValidationError)(f: PartialFunction[A, B]): Reads[B] = ...
def orElse(v: Reads[A]): Reads[A] = ...
def compose[B <: JsValue](rb: Reads[B]): Reads[A] = ...
def andThen[B](rb: Reads[B])(implicit witness: A <:< JsValue): Reads[B] = ...
}
Reads
のコンパニオンオブジェクトには以下のような apply
が実装されているので、これを利用して自分で Reads
を定義することができます。
object Reads extends ConstraintReads with PathReads with DefaultReads with GeneratedReads {
...
def apply[A](f: JsValue => JsResult[A]): Reads[A] =
new Reads[A] { def reads(json: JsValue) = f(json) }
...
}
例えば単純ですが “name” だけを持つ json から Name
クラスのオブジェクトへと変換する処理は以下のように書けます (ただ json を扱う上で必要になる Reads
(e.g. String, Int) は既に play-json で定義されているので、実際は 複雑な Reads, Writes の作成 で見るような方法の方がスッキリします)。
Name
クラス
import play.api.libs.json._
class Name(val value: String)
object Name {
// JsValue => JsResult
implicit val reads = Reads[Name] { json =>
(json \ "name") match {
case JsDefined(name) => JsSuccess(new Name(name.as[String]))
case err @ JsUndefined() => JsError(err.toString)
}
}
}
変換処理
val nameJson = Json.obj("name" -> "Alice")
println(nameJson.as[Name].value) // Alice
println(nameJson.validate[Name]) // JsSuccess(com.tiqwab.example.json.Name@318ba8c8,)
println(Json.fromJson[Name](nameJson)) // JsSuccess(com.tiqwab.example.json.Name@318ba8c8,)
val illegalNameJson = Json.obj("xxx" -> "Alice")
println(illegalNameJson.validate[Name]) // JsError(List((,List(JsonValidationError(List(JsUndefined('name' is undefined on object: {"xxx":"Alice"})),WrappedArray())))))
as
は失敗時に例外を投げるので、validate
を使用するほうが安全です。validate
は JsSuccess
または JsError
値コンストラクタを持つ JsResult
型を返します。
またケースクラスの場合は Json.reads
によりマクロで Reads
の実装が提供できます。
case class Age(age: Int)
object Age {
// Parse json like {"age" -> 20}
implicit val reads = Json.reads[Age]
}
Writes 型クラス
Writes
型クラスを実装したクラスは json へと変換することができるようになります。
Reads
と同様にこちらは writes(o: A): JsValue
を実装すれば ok で、他はデフォルト実装が存在します。
オブジェクトから json への変換には OWrites
という型クラスもあるようなのですが、これが別途用意されている理由はわかっていないです。
trait Writes[-A] {
/**
* Convert the object into a JsValue
*/
def writes(o: A): JsValue
def transform(transformer: JsValue => JsValue): Writes[A] = ...
def transform(transformer: Writes[JsValue]): Writes[A] = ...
}
trait OWrites[-A] extends Writes[A] {
def writes(o: A): JsObject
def transform(transformer: JsObject => JsObject): OWrites[A] = ...
def transform(transformer: OWrites[JsObject]): OWrites[A] = ...
}
上で使用した Name
, Age
に Writes
を実装してみます。
import play.api.libs.json._
class Name(val value: String)
object Name {
...
implicit val writes = Writes[Name] { name =>
Json.obj("name" -> name.value)
}
}
import play.api.libs.json.{Json, Reads}
case class Age(age: Int)
object Age {
...
implicit val writes = Json.writes[Age] // Add writes by macro
}
Json.toJson
で implicit に定義した Writes
を使用して json への変換が行なえます。
val name = new Name("Alice")
println(Json.toJson(name)) // {"name":"Alice"}
val age = Age(20)
println(Json.toJson(age)) // {"age":20}
Format 型クラス
上で Reads
, Writes
型クラスを別々に定義しましたが、これをペアにした 型クラスとして Format
というものがあります。
trait Format[A] extends Writes[A] with Reads[A]
trait OFormat[A] extends OWrites[A] with Reads[A] with Format[A]
object Format extends PathFormat with ConstraintFormat with DefaultFormat {
val constraints: ConstraintFormat = this
val path: PathFormat = this
implicit val invariantFunctorFormat: InvariantFunctor[Format] =
new InvariantFunctor[Format] {
def inmap[A, B](fa: Format[A], f1: A => B, f2: B => A) =
Format(fa.map(f1), Writes(b => fa.writes(f2(b))))
}
def apply[A](fjs: Reads[A], tjs: Writes[A]): Format[A] = new Format[A] {
def reads(json: JsValue) = fjs.reads(json)
def writes(o: A) = tjs.writes(o)
}
}
例えばケースクラスへの定義時に Json.Format
を使用すれば自動的に Reads
, Writes
に対応したクラスを作成することができます。
case class Age(age: Int)
object Age {
implicit val format = Json.format[Age]
}
5. 複雑な Reads, Writes の作成
JSON Reads/Writes/Formats Combinators を使用します。
Reads
例えば簡単な例として以下の Person クラスを考えます。
case class Person(name: String, age: Int) {}
以下のような json から Person を作成してみます。
{
"name": "Alice",
"age": 20
}
object ReadsCombinator extends App {
// To show types
// val namePath: JsPath = JsPath \ 'name
// val nameReads: Reads[String] = namePath.read[String]
// val builderReads: FunctionalBuilder[Reads]#CanBuild2[String, Int] =
// (JsPath \ "name").read[String] ~
// (JsPath \ "age").read[Int]
implicit val personReads: Reads[Person] = (
(JsPath \ "name").read[String] ~
(JsPath \ "age").read[Int]
)(Person.apply _)
val json = Json.obj("name" -> "Alice", "age" -> 20)
json.validate[Person] match {
case s: JsSuccess[Person] => println(s.get) // Person(Alice, 20)
case e: JsError => println(e)
}
}
やっていることは何となくわかりますが、 personReads: Reads[Person]
の定義周りの動作が捉えにくく感じます。
この部分を理解しようと思うとじっくり腰を据えないと厳しそうなのですが、ここではざっくりと何が起きているのかだけでも理解したいところです。
そこでこの部分を
FunctionalBuilder[Reads]#CanBuild2[String, Int]
を生成するReads[Person]
を生成する
の 2 つの処理に分解して整理してみます。
play-json のコードを追った感じ、FunctionalBuilder[Reads]#CanBuild2[String, Int]
は以下のような implicit 定義が使用されて生成されているようです。
Applicative[M]
であればFunctionalCanBuild[M]
に implicit conversion できる
package play.api.libs.functional
object FunctionalCanBuild {
implicit def functionalCanBuildApplicative[M[_]](implicit app: Applicative[M]): FunctionalCanBuild[M] = new FunctionalCanBuild[M] {
def apply[A, B](a: M[A], b: M[B]): M[A ~ B] = app.apply(app.map[A, B => A ~ B](a, a => ((b: B) => new ~(a, b))), b)
}
}
FunctionalCanBuild[M]
ならばFunctionalBuilderOps[M[_], A]
に implicit conversion できるFunctionalBuilderOps[M[_], A]
ならば~
からFunctionalBuilder[M]#CanBuild2[A, B]
が生成できる
package play.api.libs.functional
class FunctionalBuilderOps[M[_], A](ma: M[A])(implicit fcb: FunctionalCanBuild[M]) {
def ~[B](mb: M[B]): FunctionalBuilder[M]#CanBuild2[A, B] = {
val b = new FunctionalBuilder(fcb)
new b.CanBuild2[A, B](ma, mb)
}
def and[B](mb: M[B]): FunctionalBuilder[M]#CanBuild2[A, B] = this.~(mb)
}
Reads
は Applicative
package play.api.libs.json
object Reads extends ConstraintReads with PathReads with DefaultReads with GeneratedReads {
...
import play.api.libs.functional._
implicit def applicative(
implicit applicativeJsResult: Applicative[JsResult]
): Applicative[Reads] = new Applicative[Reads] {
def pure[A](a: A): Reads[A] = Reads.pure(a)
def map[A, B](m: Reads[A], f: A => B): Reads[B] = m.map(f)
def apply[A, B](mf: Reads[A => B], ma: Reads[A]): Reads[B] = new Reads[B] {
def reads(js: JsValue) = applicativeJsResult(mf.reads(js), ma.reads(js))
}
...
}
FunctionalBuilder[Reads]#CanBuild2[String, Int]
からの Reads[Person]
生成には以下の apply
が使用されているようです。
package play.api.libs.functional
class FunctionalBuilder[M[_]](canBuild: FunctionalCanBuild[M]) {
class CanBuild2[A1, A2](m1: M[A1], m2: M[A2]) {
def ~[A3](m3: M[A3]) = new CanBuild3[A1, A2, A3](canBuild(m1, m2), m3)
def apply[B](f: (A1, A2) => B)(implicit fu: Functor[M]): M[B] =
fu.fmap[A1 ~ A2, B](canBuild(m1, m2), { case a1 ~ a2 => f(a1, a2) })
...
}
...
}
Writes
Reads で力尽きたのですが、 Writes も考え方はまるっと同じはずです…
import play.api.libs.json._
import play.api.libs.functional.syntax._
object WritesCombinator extends App {
// def play.api.libs.functional.syntax.unlift[A, B](f: A => Option[B]): (A) => B
implicit val personReads: Writes[Person] = (
(JsPath \ "name").write[String] ~
(JsPath \ "age").write[Int]
)(unlift(Person.unapply))
val json = Json.obj("name" -> "Alice", "age" -> 20)
val person = Person("Alice", 20)
println(Json.toJson(person)) // {"name":"Alice","age":20}
}
最後にこれらを使用して冒頭の json に対応する Person
クラスに Reads
と Writes
を与えます。
Person
, FamilyMember
モデルの作成
import play.api.libs.json.{JsPath}
import play.api.libs.functional.syntax._
case class Person(
name: String,
age: Int,
favorites: Seq[String],
family: Seq[FamilyMember]
)
object Person {
// Define by macro
// implicit val format = Json.format[Person]
// Define manually
implicit val format = (
(JsPath \ "name").format[String] ~
(JsPath \ "age").format[Int] ~
(JsPath \ "favorites").format[Seq[String]] ~
(JsPath \ "family").format[Seq[FamilyMember]]
)(Person.apply _, unlift(Person.unapply _))
}
import play.api.libs.json.Json
case class FamilyMember(name: String, relation: String)
object FamilyMember {
implicit val format = Json.format[FamilyMember]
}
Person
オブジェクトと json の相互変換
import play.api.libs.json._
object ComplicatedReadsCombinator extends App {
val rawString =
"""
|{
| "name": "Alice",
| "age": 20,
| "favorites": ["tennis", "swimming"],
| "family": [
| {
| "name": "Bob",
| "relation": "father"
| },
| {
| "name": "Catharin",
| "relation": "mother"
| }
| ]
|}
""".stripMargin
val json = Json.parse(rawString)
val person = json.validate[com.tiqwab.example.json.model.Person].get
println(person) // Person(Alice,20,List(tennis, swimming),List(FamilyMember(Bob,father), FamilyMember(Catharin,mother)))
println(Json.stringify(Json.toJson(person))) // {"name":"Alice","age":20,"favorites":["tennis","swimming"],"family":[{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}]}
}