Documentation

You are viewing the documentation for the 2.1.x release series. The latest stable release series is 2.4.x.

§JSON Reads[T]/Writes[T]/Format[T] コンビネータ

このドキュメントは、当初 Pascal Voitot (@mandubian) の記事 mandubian.com として公開されたものです

§Play2.1 における新機能の概要

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

val customReads: Reads[(String, Float, List[String])] = 
  (JsPath \ "key1").read[String](email keepAnd minLength(5)) and 
  (JsPath \ "key2").read[Float](min(45)) and
  (JsPath \ "key3").read[List[String]] 
  tupled
import play.api.libs.json.Json

val js = Json.obj(
  "key1" -> "alpha", 
  "key2" -> 123.345F, 
  "key3" -> Json.arr("alpha", "beta")
)

scala> customReads.reads(js) 
res5: JsSuccess(("alpha", 123.345F, List("alpha", "beta")))

customReads.reads(js).fold(
  invalid = { errors => ... },
  valid = { res => 
    val (s, f, l): (String, Float, List[String]) = res
    ...
  }
)
		

それでは詳細に踏み込んでみましょう ;)



§JsPath とは

パスに基づいたシンプルな構文を使って XML 抽象構文木のノードにアクセスすることのできる XMLPath についてはご存知でしょう。
JSON は抽象構文木なので、同じような構文を適用することができます。そして、これを論理的に JsPath と呼んでいます。

以下のすべての例では、ひとつ前の段落で定義した JSON を使います。

§JsPath を構築する

import play.api.libs.json._

// シンプルなパス
JsPath \ "key1"

// 二階層のパス
JsPath \ "key3" \ "key33"
 
// インデックス付けされたパス
(JsPath \ "key3" \ "key32")(2) // JsArray の二番目の要素

// 複数の/再帰的なパス
JsPath \\ "key1"

§代替構文

JsPath という構文もかなりかっこいいのですが、コード中において Reads[T] をよりクリアに目立たせることのできる構文を見つけました。
このような理由から、JsPath のエイリアスを提供しています: __ (二つのアンダースコア) です。
この構文を使っても使わなくても構いません。この構文は、コードの中から JsPath をすぐに見つけられるようにする、視覚的な利便性に過ぎません

次のように書くことができます:

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

// シンプルなパス
__ \ "key1"

// 二階層のパス
__ \ "key3" \ "key33"
 
// インデックス付けされたパス
(__ \ "key3" \ "key32")(2) // JsArray の二番目の要素

// 複数のパス
__ \\ "key1"

// 双方の構文の違いを紹介する Reads[T] コンビネータのサンプル
// 今すぐこのコードを理解しようとしないでください… これは次の段落で説明します
val customReads = 
  (JsPath \ "key1").read(
    (JsPath \ "key11").read[String] and
    (JsPath \ "key11").read[String] and
    (JsPath \ "key11").read[String]
    tupled
  ) and 
  (JsPath \ "key2").read[Float](min(45)) and
  (JsPath \ "key3").read(
    (JsPath \ "key31").read[String] and
    (JsPath \ "key32").read[String] and
    (JsPath \ "key33").read[String]
    tupled
  ) 
  tupled
  
// __ を使った場合
val customReads = 
  (__ \ "key1").read(
    (__ \ "key11").read[String] and
    (__ \ "key11").read[String] and
    (__ \ "key11").read[String]  
    tupled
  ) and 
  (__ \ "key2").read[Float](min(45)) and
  (__ \ "key3").read[List[String]] (
    (__ \ "key31").read[String] and
    (__ \ "key32").read[String] and
    (__ \ "key33").read[String]
    tupled  
  )
  tupled
  
// JSON ツリーの構造がひと目で分かりますね

§JsPath の値へのアクセス

与えられた JsPath にある JsValue から値を取り出す重要な関数は以下のとおりです:

sealed trait PathNode {
  def apply(json: JsValue): List[JsValue]
  …
}

ご覧のとおり、この関数は List[JsValue] を取り出します

これは、以下のようにシンプルに使うことができます:

import play.api.libs.json._

// JsPath を構築する
scala> (__ \ "key1")(js) 
res12: List[play.api.libs.json.JsValue] = List("value1")  // これは実際には JsString("value1") です

// 二階層のパス
scala> (__ \ "key3" \ "key33")(js)
res13: List[play.api.libs.json.JsValue] = List({"key":"value2","key34":"value34"})
 
// インデックス付けされたパス
scala> (__ \ "key3" \ "key32")(2)(js)
res14: List[play.api.libs.json.JsValue] = List(234.13)

// 複数のパス
scala> (__ \\ "key1")(js)
res17: List[play.api.libs.json.JsValue] = List("value1", "value2")


§Reads[T] はバリデータに

§Play2.0.x における Reads

Play2.0.x では、どのように Json Reads[T] を書いたか覚えていますか?
reads 関数をオーバーライドする必要がありました。

trait Reads[A] {
  self =>
  /**
   * JsValue を A に変換する
   */
  def reads(json: JsValue): A
}

以下のような、JSON 構造をマッピングするシンプルなケースクラスを取り上げます:

case class Creature(
  name: String, 
  isDead: Boolean, 
  weight: Float
)

Play2.0.x では、以下のような reader を書いていたことでしょう:

import play.api.libs.json._

implicit val creatureReads = new Reads[Creature] {
  def reads(js: JsValue): Creature = {
    Creature(
      (js \ "name").as[String],
      (js \ "isDead").as[Boolean],
      (js \ "weight").as[Float]
    )
  }
}

scala> val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
scala> val c = js.as[Creature] 
c: Creature("gremlins", false, 1.0F)

簡単ですよね?
こんなに簡単なら、何が問題なのでしょう?

以下のような、フィールドが不足した JSON を渡すことを想像してください:

val js = Json.obj( "name" -> "gremlins", "weight" -> 1.0F)

なにが起こるでしょう?

java.lang.RuntimeException: Boolean expected
	at play.api.libs.json.DefaultReads$BooleanReads$.reads(Reads.scala:98)
	at play.api.libs.json.DefaultReads$BooleanReads$.reads(Reads.scala:95)
	at play.api.libs.json.JsValue$class.as(JsValue.scala:56)
	at play.api.libs.json.JsUndefined.as(JsValue.scala:70)

これは何でしょう?
そうです、みっともない (サブクラスですらない) RuntimeException です。しかし、JsValue.asOpt[T] を使ってこれに対処することができます :)

scala> val c: Option[Creature] = js.asOpt[Creature]
c: None

クールですが、分かるのは Json => Creature のデシリアライズに失敗したことだけで、どこ、あるいはどのフィールドで失敗したのかは分かりません



§Play2.1 における Reads

この不完全な API を Play2.1 でそのままにしておけず、 Reads[T] API を以下のように変更しました :

trait Reads[A] {
  self =>
  /**
   * JsValue を A に変換する
   */
  def reads(json: JsValue): JsResult[A]
}

そうです、すでに存在する自作の Reads は、すべてリファクタリングしなければなりません。しかし、たくさんの興味深い新機能が手に入ることがすぐに分かると思います…

すぐに、今回の問題に適用した Either にちょっと似ているように見える、とてもシンプルな構造の JsResult[A] に目が行ったことと思います。

JsResult[A] は二つのタイプになり得ます:

case class JsSuccess[A](
  value: T, // JsValue => A のデシリアライズが動作したときに取り出される値
  path: JsPath = JsPath() // この A が JsValue に読み込まれたときのルートの JsPath (デフォルトでは JsValue のルート)
) extends JsResult[T]

// 値から JsSuccess を作るには、次のようにシンプルに行います:
val success = JsSuccess(Creature("gremlins", false, 1.0))

JsError は累積エラーであり、異なる JsPath にある Json で検出された複数のエラーを保存することができます。この素晴らしい利点に注目してください

case class JsError(
  errors: Seq[(JsPath, Seq[ValidationError])]  
  // errors は、この JsValue のどこにエラーがあったのか、
  // このパスのどこにバリデーションエラーがあったのかを
  // 指し示す JsPath のシーケンスです
) extends JsResult[Nothing]

// ValidationError は、引数を伴った (ローカライズされたメッセージにマッピングできる) メッセージです
case class ValidationError(message: String, args: Any*)

// JsError を作るために、例えば次のようないくつかのヘルパーがあります
val errors1 = JsError( __ \ 'isDead, ValidationError("validate.error.missing", "isDead") )
val errors2 = JsError( __ \ 'name, ValidationError("validate.error.missing", "name") )

// とても興味深いことに、Errors は累積することができます
scala> val errors = errors1 ++ errors2
errors: JsError(List((/isDead,List(ValidationError(validate.error.missing,WrappedArray(isDead)))), (/name,List(ValidationError(validate.error.missing,WrappedArray(name))))))

さて、ここで興味深いのは、JsResult[A] はモナディックな構造であり、この構造の馴染み深い関数と共に使うことができるということです:

次のような糖衣構文と使うこともできます :

エラーを累積するので、 JsResult[A] はモナディックなだけでなくアプリカティブである ことにも注目してください。
この累積する機能のため、JsResult[T]for 内包表記 で使うことはあまり良いことではありません。すべてのエラーではなく、最初のひとつだけを取り出すことになるからです。

§Reads[A] はバリデータに

ご理解頂いたとおり、新しい Reads[A] を使うと、ただ JsValue を別の構造にデシリアライズするだけではなく、実際にその JsValue の バリデーションを行い 、そしてすべてのバリデーションエラーを探し出します。
ところで、JsValuevalidate と呼ばれる新しい関数が登場しました:

trait JsValue {
…
  def validate[T](implicit _reads: Reads[T]): JsResult[T] = _reads.reads(this)
  
  // 以前と同じように振る舞いますが、具体的な実行時例外 JsResultException を投げるようになりました
  def as[T](implicit fjs: Reads[T]): T
  // 以前とまったく同じように振る舞います
  def asOpt[T](implicit fjs: Reads[T]): Option[T]
…
}

// スコープから正しい implicit を取得できる場合に、このように書けるようになります
val res: JsResult[Creature] = js.validate[Creature])

§JsResult[A] の操作

JsResult を操作する際は、値に直接アクセスするのではなく、map/flatmap/fold を使って値を変更することが推奨されています。

import play.api.libs.json._

val res: JsResult[Creature] = js.validate[Creature]

// 成功/失敗を管理し、何かしらの結果を返します
res.fold(
  valid = { c => println( c ); c.name },
  invalid = { e => println( e ); e }
)

// name を直接取得します (JsResult が JsError の場合、get は NoSuchElementException を投げることができます)
val name: JsSuccess[String] = res.map( creature => creature.name ).get

// 結果をフィルタリングします
val name: JsSuccess[String] = res.filter( creature => creature.name == "gremlins" ).get

// お馴染みの Play アクション
def getNameOnly = Action(parse.json) { request =>
  val json = request.body
  json.validate[Creature].fold(
    valid = ( res => Ok(res.name) ),
    invalid = ( e => BadRequest(e.toString) )
  )
}

§Reads の興味深い新機能

ご存知のとおり、Play2.1 の Json API はまだ草案であり、この記事を書き始めたときからも進化しています。
その後、いくつかの (概念的ではなく、表面的な) 変更が加えられました。

§Reads[A <: JsValue] andThen Reads[B]

andThen には、関数合成に関する馴染み深い Scala のセマンティックが含まれています : JSON に Reads[A <: JsValue] を適用して JsValue を取り出し、それからこの JsValue に Reads[B] を適用します。

§Reads[A <: JsValue].map(f: A => B): Reads[B]

map は馴染み深く、そしていつでもとても便利な Scala の map 関数です。

§Reads[A <: JsValue].flatMap(f: A => Reads[B]): Reads[B]

flatMap は、馴染み深い Scala の flatMap 関数です。

§JsResult[A] で Reads[T] を書き直す

検出されたすべてのエラーをかき集めた JsResult を返さなければならないので、JsResult を返す Reads[A] API を以前の Reads[A] のように書くことはできません。
シンプルに flatMap で Reads[T] を組み立てるところを想像するかもしれません :

以下のコードは誤りです

import play.api.libs.json._

// 誤ったコードです。使わないでください
implicit val creatureReads = new Reads[Creature] {
  def reads(js: JsValue): JsResult[Creature] = {
    (js \ "name").validate[String].flatMap{ name => 
      (js \ "isDead").validate[Boolean].flatMap { isDead =>
      (js \ "weight").validate[Float].map { weight =>
        Creature(name, isDead, weight)
      }
    }
  }
  }
}

JsResult の主な目的は、JsValue のバリデーションを行っている間に見つかったすべてのエラーをかき集めることであることを思い出してください。

JsResult.flatMap は純粋にモナディックな関数 (何のことか分からなくても、理解できるので気にしないでください) なので、flatMap() に渡した関数は、その結果が JsSuccess であるときのみ呼び出され、そうでない場合はただ JsError を返します。
これは、上記のコードはバリデーションを行っている間に見つかったすべてのエラーをかき集めるわけではなく、最初のエラーで止まってしまうことを意味しており、これはまったく望ましくありません。

実際のところ、ただ Reads を組み立てるだけではなく、以下のようなスキーマに従って結合することを期待しているので、このような場合においてモナドパターンは好ましくありません:

Reads[String] AND Reads[Boolean] AND Reads[Float]   
  => Reads[(String, Boolean, Float)]   
     => Reads[Creature]

このため、Reads を結合できるようにする何かが必要であり、これこそが Play2.1 が JSON のために用意した素晴らしい新機能 : JsPath を伴う READS コンビネータ です。


この要件に適応するように総称的な関数型の構造に基づいて実装する方法の、より論理的な側面を知りたい場合は、@sadache の書いたこのポスト “アプリカティブは制約し過ぎ。アプリカティブの打破と関数型ビルダの紹介” で読むことができます。

§Reads[T] コンビネータを書く

§コンビネータを使うための最小インポート

// スコープ内に Json 構造が必要な場合
import play.api.libs.json._
// 重要。必要なツールをスコープにインポートします
import play.api.libs.functional.syntax._

§コンビネータで Reads[T] を書き直す

いきなりサンプルに飛び込んで実践するのが往々にして最良です :

// 重要。必要なツールをスコープにインポートします
import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit val creatureReads = (
  (__ \ "name").read[String] and
  (__ \ "isDead").read[Boolean] and
  (__ \ "weight").read[Float]
)(Creature.apply _)  

// または、apply 関数を持つコンパニオンオブジェクトと共にケースクラスとするのもシンプルな方法です
implicit val creatureReads = (
  (__ \ "name").read[String] and
  (__ \ "isDead").read[Boolean] and
  (__ \ "weight").read[Float]
)(Creature)  

// あるいは、Scala のパーサコンビネータを知っている方々は、これに着想を得た演算子を使います
implicit val creatureReads = (
  (__ \ "name").read[String] ~
  (__ \ "isDead").read[Boolean] ~
  (__ \ "weight").read[Float]
)(Creature)  

どうでしょう、難しいことは何もありませんよね?

§(__ \ "name")read[String] を適用しようとしている JsPath です


§andReads[A] and Reads[B] => Builder[Reads[A ~ B]] を意味する演算子に過ぎません

§(…)(Creature)Reads[Creature] を作ります

(__ \ "name").read[String] and (__ \ "isDead").read[Boolean] and (__ \ "weight").read[Float]

これは、以下のものを作ります

Builder[Reads[String ~ Boolean ~ Float])]

しかし、期待しているのは Reads[Creature] です。

以下を試してみてください:

scala> val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
scala> js.validate[Creature] 
res1: play.api.libs.json.JsResult[Creature] = JsSuccess(Creature(gremlins,false,1.0),) 
// この JsPath はデフォルトでルートになるので、最後のカンマの後には何もありません

さて、ここでエラーがあった場合は何が起こるのでしょう?

scala> val js = Json.obj( "name" -> "gremlins", "weight" -> 1.0F)
scala> js.validate[Creature] 
res2: play.api.libs.json.JsResult[Creature] = JsError(List((/isDead,List(ValidationError(validate.error.missing-path,WrappedArray())))))

分かり易いでしょう?

§複雑化された場合

ええ、あなたの考えていることは分かっています : もっと複雑な場合はどうでしょう。フィールドに複数の制約があって、Json の中に Json が組み込まれていて、再帰的なクラスで…

例に取り上げている生き物について想像してみましょう:

クラスは以下のようになります:

case class Creature(
  name: String, 
  isDead: Boolean, 
  weight: Float,
  email: String, // email フォーマットかつ minLength(5)
  favorites: (String, Int), // 間抜けなお気に入りデータ
  friends: List[Creature] = Nil, // ええ、デフォルトでは友達がいません
  social: Option[String] = None // デフォルトでは社交的ではありません
)

Play2.1 は多くの総称的な Reads ヘルパーを提供しています:

// Reads ヘルパーをスコープにインポートします
import play.api.libs.json._
import play.api.libs.functional.syntax._
import play.api.data.validation.ValidationError

// 再利用するために独自の Reads を定義します
// この Reads は、文字列が与えられた文字列と等しくないことを検証します
def notEqualReads[T](v: T)(implicit r: Reads[T]): Reads[T] = Reads.filterNot(ValidationError("validate.error.unexpected.value", v))( _ == v )

def skipReads(implicit r: Reads[String]): Reads[String] = r.map( _.substring(2) )

implicit val creatureReads: Reads[Creature] = (
  (__ \ "name").read[String] and
  (__ \ "isDead").read[Boolean] and
  (__ \ "weight").read[Float] and
  (__ \ "email").read(email keepAnd minLength[String](5)) and
  (__ \ "favorites").read( 
    (__ \ "string").read[String]( notEqualReads("ni") andKeep skipReads ) and
    (__ \ "number").read[Int]( max(86) or min(875) )
    tupled
  ) and
  (__ \ "friends").lazyRead( list[Creature](creatureReads) ) and
  (__ \ "social").readNullable[String]
)(Creature)  

上記の多くのことは論理的に理解できますが、ちょっと説明してみましょう:

§(__ \ "email").read(email keepAnd minLength[String](5))

§notEqualReads("ni") andKeep skipReads

§max(86) or min(875)

§(__ \ "favorites").read(…)

    (__ \ "string").read[String]( notEqualReads("ni") andKeep notEqualReads("swallow") ) and
    (__ \ "number").read[Int]( max(86) or min(875) )
    tupled

§(__ \ "friend").lazyRead( list[Creature](creatureReads) )

これがこのコードでもっとも複雑な行です。しかし、なぜ複雑なのかは理解できます。friendCreature クラス自身において再帰的なフィールドであり、特別な取り扱いが必要です。

§(__ \ "social").readNullable[String]

理解するのに複雑なところは何もありません: option を読み込む必要があり、readNullable はこれを手助けしてくれます。

これで、この Reads[Creature] を使うことができます

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

val gizmojs = Json.obj( 
  "name" -> "gremlins", 
  "isDead" -> false, 
  "weight" -> 1.0F,
  "email" -> "[email protected]",
  "favorites" -> Json.obj("string" -> "alpha", "number" -> 85),
  "friends" -> Json.arr(),
  "social" -> "@gizmo"
)

scala> val gizmo = gizmojs.validate[Creature] 
gizmo: play.api.libs.json.JsResult[Creature] = JsSuccess(Creature(gremlins,false,1.0,[email protected],(pha,85),List(),Some(@gizmo)),)

val shaunjs = Json.obj( 
  "name" -> "zombie", 
  "isDead" -> true, 
  "weight" -> 100.0F,
  "email" -> "[email protected]",
  "favorites" -> Json.obj("string" -> "brain", "number" -> 2),
  "friends" -> Json.arr( gizmojs))

scala> val shaun = shaunjs.validate[Creature] 
shaun: play.api.libs.json.JsResult[Creature] = JsSuccess(Creature(zombie,true,100.0,[email protected],(ain,2),List(Creature(gremlins,false,1.0,[email protected],(alpha,85),List(),Some(@gizmo))),None),)

val errorjs = Json.obj( 
  "name" -> "gremlins", 
  "isDead" -> false, 
  "weight" -> 1.0F,
  "email" -> "rrhh",
  "favorites" -> Json.obj("string" -> "ni", "number" -> 500),
  "friends" -> Json.arr()
)

scala> errorjs.validate[Creature] 
res0: play.api.libs.json.JsResult[Creature] = JsError(List((/favorites/string,List(ValidationError(validate.error.unexpected.value,WrappedArray(ni)))), (/email,List(ValidationError(validate.error.email,WrappedArray()), ValidationError(validate.error.minlength,WrappedArray(5)))), (/favorites/number,List(ValidationError(validate.error.max,WrappedArray(86)), ValidationError(validate.error.min,WrappedArray(875))))))

§Reads[A] のその他の機能

§(Reads[A] and Reads[B]).tupled: Reads[(A, B)]

これは Reads[TupleX] を作るのに便利です

(
  (__ \ 'field1).read[String] and 
  (__ \ 'field2).read[Int]
).tupled : Reads[(String, Int)]

インデックスが指定された JsArray と使うこともできます

(
  (__(0)).read[String] and 
  (__(1)).read[Int]
).tupled : Reads[(String, Int)]

§(Reads[A1 <: A] and Reads[A2 <: A]).reduce(implicit reducer: Reducer[A, B]): Reads[B]

JSON のいくつかの部分を読み込んで集約するのに便利です。
これには implicit な Reducer/Monoid が必要です。
JsObjectJsArray 向けのものが提供されています。

以下は、次の段落で登場する Json トランスフォーマーのいくつかの使用例です:

§JsObject を Reduce する (ブランチをコピーして、ひとつの JsObuject に集約する)

(
  (__ \ 'field1).json.pickBranch[JsString] and 
  (__ \ 'field2).json.pickBranch[JsNumber]
).reduce : Reads[JsObject]

§JsArray を Reduce する (リーフをコピーして、ひとつの JsArray に集約する)

(
  (__ \ 'field1).json.pick[JsString] and 
  (__ \ 'field2).json.pick[JsNumber]
).reduce : Reads[JsArray]


§Writes[T] は (コンビネータ以外) 変更なし

§Play2.0.x における Writes

Play2.0.x では Json Writes[T] をどのように書かなければならなかった覚えていますか?
writes 関数をオーバーライドする必要がありました。

trait Writes[-A] {
  self =>
  /**
   * オブジェクトを JsValue に変換する
   */
  def writes(o: A): JsValue
}

Part1 で使ったものと同じシンプルなケースクラスを取り上げましょう:

case class Creature(
  name: String, 
  isDead: Boolean, 
  weight: Float
)

Play2.0.x では Writes[Creature] を以下のように書いていたことでしょう (Play2.0.x には存在していませんでしたが、もう一度紹介するために新しい Json 文法を使っています ;) ):

import play.api.libs.json._

implicit val creatureWrites = new Writes[Creature] {
  def writes(c: Creature): JsValue = {
    Json.obj(
    	"name" -> c.name,
    	"isDead" -> c.isDead,
    	"weight" -> c.weight
    )
  }
}

scala> val gizmo = Creature("gremlins", false, 1.0F)
scala> val gizmojs = Json.toJson(gizmo)
gizmojs: play.api.libs.json.JsValue = {"name":"gremlins","isDead":false,"weight":1.0}

§Play2.1.x における Writes

不安に思うことはありません: Play2.1 では、まったく同じ方法で Writes を書きます :D

それでは何が違うのでしょう?
Part1 で述べられた通り、Reads はシンプルな論理演算子を使って結合することができました。
Scala の関数型の力を使うことで、**Writes[T] コンビネータを提供** できるようになりました。

この要件に適応するように総称的な関数型の構造に基づいて実装する方法の、より論理的な側面を知りたい場合は、@sadache の書いたこのポスト “アプリカティブは制約し過ぎ。アプリカティブの打破と関数型ビルダの紹介” で読むことができます。

§Writes の主な変更点: コンビネータ

再びコードから始めましょう: 以前の Writes[T] をコンビネータを使って書き直します。

// 重要。必要なツールをスコープにインポートします
import play.api.libs.json._
// 必要な関数型の総称的な構造をインポートします
import play.api.libs.functional.syntax._

implicit val creatureWrites = (
  (__ \ "name").write[String] and
  (__ \ "isDead").write[Boolean] and
  (__ \ "weight").write[Float]
)(unlift(Creature.unapply))

// あるいは、Scala のパーサコンビネータを知っている人は、これに着想を得た演算子を使います
implicit val creatureWrites = (
  (__ \ "name").write[String] ~
  (__ \ "isDead").write[Boolean] ~
  (__ \ "weight").write[Float]
)(unlift(Creature.unapply))

scala> val c = Creature("gremlins", false, 1.0F)
scala> val js = Json.toJson(c)
js: play.api.libs.json.JsValue = {"name":"gremlins","isDead":false,"weight":1.0}

いくつかの点を除いて、Reads[T] にとてもよく似ているでしょう?
ちょっと説明してみましょう (Reads の記事をコピーしてちょっとだけ変更して… 面倒くさがりなんです ;)):

§import play.api.libs.json.Writes._

その他のインポートに影響するものを除いて、Writes[T] に必要なものだけをインポートします。

§(__ \ "name").write[String]

この JsPath に write[String] を適用します (Reads とまったく同じです)

§andWrites[A] and Writes[B] => Builder[Writes[A ~ B]] を意味する演算子に過ぎません

§(…)(unlift(Creature.unapply))Writes[Creature] を作ります

(__ \ "name").write[String] and (__ \ "isDead").write[Boolean] and (__ \ "weight").write[Float]` 

これは、以下のものを作ります

Builder[Writes[String ~ Boolean ~ Float])]` しかし、欲しいのは `Writes[Creature] です

気に留めておかなければならない唯一のことは、この unlift 呼び出しが自然でないように見えるのは最初だけということです!

推測されている通り、書き込みの際にはバリデーションを行わず、エラー処理がまったく無いため、Writes[T]Reads[T] よりずっと簡単です。

さらに、このため Writes[T] に提供されている演算子は Reads[T] ほど豪華でないことを気に留めておかなければなりません。keepAndandKeep を覚えていますか? これらは Writes[T] において何の意味もありません。A~B を書き込むときは、A and B を書くのであって、only A or only B ではありません。このため、and がただ一つ Writes[T] に提供されている演算子です。

§複雑化された場合

Part1 で使った、もっと複雑な例に立ち返ってみましょう。
以下のようにモデリングした生き物を想像したことを思い出してください:

case class Creature(
  name: String, 
  isDead: Boolean, 
  weight: Float,
  email: String, // email フォーマットかつ minLength(5)
  favorites: (String, Int), // 間抜けなお気に入りデータ
  friends: List[Creature] = Nil, // ええ、デフォルトでは友達がいません
  social: Option[String] = None // デフォルトでは社交的ではありません
)

これに対応する Writes[Creature] を書いてみましょう。

// 重要。必要なツールをスコープにインポートします
import play.api.libs.json._
// 必要な関数型の総称的な構造をインポートします
import play.api.libs.json.functional.syntax._

implicit val creatureWrites: Writes[Creature] = (
  (__ \ "name").write[String] and
  (__ \ "isDead").write[Boolean] and
  (__ \ "weight").write[Float] and
  (__ \ "email").write[String] and
  (__ \ "favorites").write( 
    (__ \ "string").write[String] and
    (__ \ "number").write[Int]
    tupled
  ) and
  (__ \ "friends").lazyWrite(Writes.traversableWrites[Creature](creatureWrites)) and
  (__ \ "social").write[Option[String]]
)(unlift(Creature.unapply))  

val gizmo = Creature("gremlins", false, 1.0F, "[email protected]", ("alpha", 85), List(), Some("@gizmo"))
val gizmojs = Json.toJson(gizmo)
gizmojs: play.api.libs.json.JsValue = {"name":"gremlins","isDead":false,"weight":1.0,"email":"[email protected]","favorites":{"string":"alpha","number":85},"friends":[],"social":"@gizmo"}

val zombie = Creature("zombie", true, 100.0F, "[email protected]", ("ain", 2), List(gizmo), None)
val zombiejs = Json.toJson(zombie)
zombiejs: play.api.libs.json.JsValue = {"name":"zombie","isDead":true,"weight":100.0,"email":"[email protected]","favorites":{"string":"ain","number":2},"friends":[{"name":"gremlins","isDead":false,"weight":1.0,"email":"[email protected]","favorites":{"string":"alpha","number":85},"friends":[],"social":"@gizmo"}],"social":null

とても単純であることが分かると思います。特別な演算子が無いので、Reads[T] よりはるかに簡単です。
いくつか説明します:

§(__ \ "favorites").write(…)
  (__ \ "string").write[String] and
  (__ \ "number").write[Int]
  tupled

§(__ \ "friend").lazyWrite(Writes.traversableWrites[Creature](creatureWrites))

これは Creature クラス自身の再帰的なフィールドを取り扱うための、lazyRead の対称となるコードです。

ちなみに、Writes.traversableWrites[Creature]: Writes[Traversable[Creature]]Writes[List[Creature]] に置き換えられることを不思議に思うかもしれませんね?
これは、Writes[-T] が反変的な意味をもつためです。Traversable[Creature] と書けるのであれば、Traversable を継承する List として List[Creature] を書くことができます (継承の関連は、反変性によって取り消されます) 。

§Writes[A] その他の機能

§Writes[A].contramap( B => A ): Writes[B]

Writes[A]contramap できる反変的な要素です。
このため、Writes[B] に変換するための関数 B => A を与えてやらなければなりません。

例:

scala> case class Person(name: String)
defined class Person

scala> __.write[String].contramap( (p: Person) => p.name )
res5: play.api.libs.json.OWrites[Person] = [email protected]

§(Writes[A] and Writes[B]).tupled: Writes[(A, B)]

Writes[TupleX] を作るのに便利です。

(
  (__ \ 'field1).write[String] and 
  (__ \ 'field2).write[Int]
).tupled : Writes[(String, Int)]

既知の制限 以下は動作しないことに気を付けてください: Write コンビネータ は JsObject の生成方法しか知らず、JsArray の生成方法は知らないため、以下はコンパイルされますが、実行時に落ちます。

// 注意: コンパイルされますが、実行時に落ちます
(
  (__(0)).write[String] and 
  (__(1)).write[Int]
).tupled : Writes[(String, Int)]

§(Writes[A1 <: A] and Writes[A2 <: A]).join: Writes[A]

複数のブランチに同じ値を書き込むのに便利です。

例えば:

// Write の結果の型を与えなければならないことに注意してください
scala> val jsWrites: Writes[JsString] = (
     |   (__ \ 'field1).write[JsString] and 
     |   (__ \ 'field2).write[JsString]
     | ).join
jsWrites: play.api.libs.json.Writes[play.api.libs.json.JsString] = [email protected]

scala> jsWrites.writes(JsString("toto"))
res3: play.api.libs.json.JsObject = {"field1":"toto","field2":"toto"}

§Format 用のコンビネータについて

Play2.1 には、Format[T] extends Reads[T] with Writes[T] と呼ばれる機能がありました。
これは、同じ場所にシリアライズ/デシリアライズを提供するために、Reads[T]Writes[T] をまとめてミックスしました。

Play2.1 は Reads[T]Writes[T] のコンビネータを提供します。Format[T] のコンビネータについてはどうでしょう?

とてもシンプルな例に立ち返りましょう:

case class Creature(
  name: String, 
  isDead: Boolean, 
  weight: Float
)

Reads[Creature] は、以下のように書きます:

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

val creatureReads = (
  (__ \ "name").read[String] and
  (__ \ "isDead").read[Boolean] and
  (__ \ "weight").read[Float]
)(Creature)  

implicit を使っていないので、Format[T] を定義する際はコンテクスト中に暗黙の Reads[Creature] が存在しないことに気を付けてください

Writes[Creature] は、以下のように書きます:

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

val creatureWrites = (
  (__ \ "name").write[String] and
  (__ \ "isDead").write[Boolean] and
  (__ \ "weight").write[Float]
)(unlift(Creature.unapply))  

§Format[Creature] を作るために Reads/Writes 両方をかき集めるには?

§ひとつ目の方法 = 既存の reads/writes から作る

以下のようにして、既存の Reads[T]Writes[T] を再利用して Format[T] を作ることができます:

implicit val creatureFormat = Format(creatureReads, creatureWrites)

val gizmojs = Json.obj( 
  "name" -> "gremlins", 
  "isDead" -> false, 
  "weight" -> 1.0F
)

val gizmo = Creature("gremlins", false, 1.0F)

assert(Json.fromJson[Creature](gizmojs).get == gizmo)
assert(Json.toJson(gizmo) == gizmojs)

§ふたつ目の方法 = コンビネータを使って作る

Reads と Writes のコンビネータを手に入れましたよね?
Play2.1 は関数型プログラミングの魔法による Format コンビネータ も提供します (本当は魔法ではなく、純粋な関数型プログラミングです ;))

いつもどおり、コードから始めます:

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

implicit val creatureFormat = (
  (__ \ "name").format[String] and
  (__ \ "isDead").format[Boolean] and
  (__ \ "weight").format[Float]
)(Creature.apply, unlift(Creature.unapply))  

val gizmojs = Json.obj( 
  "name" -> "gremlins", 
  "isDead" -> false, 
  "weight" -> 1.0F
)

val gizmo = Creature("gremlins", false, 1.0F)

assert(Json.fromJson[Creature](gizmojs).get == gizmo)
assert(Json.toJson(gizmo) == gizmojs)

とくに奇妙なところはありません…

§(__ \ "name").format[String]

これは、与えられた JsPath で読み/書きを行う format[String] を作ります

§( )(Creature.apply, unlift(Creature.unapply))

Scala の構造にマッピングするために:

このため、Format[Creature] extends Reads[Creature] with Writes[Creature] のように、Creature.applyunlift(Creature.unapply) を用意しました。これでおしまいです…

§もっと複雑な場合

先の例は、構造がとてもシンプルだし、読み/書きが対照的だったため、すこし間が抜けています。以下の例を考えます:

Json.fromJson[Creature](Json.toJson(creature)) == creature

この場合、書き出したいものを読み込みます。逆もまた同様です。このため、Reads[T]Writes[T] の両方をまとめて作る、とてもシンプルな JsPath.format[T] 関数を使うことができます。

しかし、よくあるもっと複雑なケースクラスを取り扱う場合は、どのように Format[T] を書くのでしょうか?

以下のコードを思い出してください:

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

// ケースクラス
case class Creature(
  name: String, 
  isDead: Boolean, 
  weight: Float,
  email: String, // email フォーマットかつ minLength(5)
  favorites: (String, Int), // 間抜けなお気に入りデータ
  friends: List[Creature] = Nil, // ええ、デフォルトでは友達がいません
  social: Option[String] = None // デフォルトでは社交的ではありません
)

import play.api.data.validation.ValidationError
import play.api.libs.json.Reads._

// 再利用するために独自の Reads を定義します
// この Reads は、文字列が与えられた文字列と等しくないことを検証します
def notEqualReads[T](v: T)(implicit r: Reads[T]): Reads[T] = Reads.filterNot(ValidationError("validate.error.unexpected.value", v))( _ == v )

def skipReads(implicit r: Reads[String]): Reads[String] = r.map( _.substring(2) )

val creatureReads: Reads[Creature] = (
  (__ \ "name").read[String] and
  (__ \ "isDead").read[Boolean] and
  (__ \ "weight").read[Float] and
  (__ \ "email").read(email keepAnd minLength[String](5)) and
  (__ \ "favorites").read( 
    (__ \ "string").read[String]( notEqualReads("ni") andKeep skipReads ) and
    (__ \ "number").read[Int]( max(86) or min(875) )
    tupled
  ) and
  (__ \ "friends").lazyRead( list[Creature](creatureReads) ) and
  (__ \ "social").read(optional[String])
)(Creature)  

import play.api.libs.json.Writes._

val creatureWrites: Writes[Creature] = (
  (__ \ "name").write[String] and
  (__ \ "isDead").write[Boolean] and
  (__ \ "weight").write[Float] and
  (__ \ "email").write[String] and
  (__ \ "favorites").write( 
    (__ \ "string").write[String] and
    (__ \ "number").write[Int]
    tupled
  ) and
  (__ \ "friends").lazyWrite(Writes.traversableWrites[Creature](creatureWrites)) and
  (__ \ "social").write[Option[String]]
)(unlift(Creature.unapply))

見ての通り、creatureReadscreatureWrites は完全に対称的ではないので、以前に行ったようにひとつの Format[Creature] にまとめることはできません。

Json.fromJson[Creature](Json.toJson(creature)) != creature

願わくば、先に行ったように Reads[T]Writes[T] から Format[T] を作りたいものです。

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

implicit val creatureFormat: Format[Creature] = Format(creatureReads, creatureWrites)

// Creature から Json へのシリアライズをテストする
val gizmo = Creature("gremlins", false, 1.0F, "[email protected]", ("alpha", 85), List(), Some("@gizmo"))
val zombie = Creature("zombie", true, 100.0F, "[email protected]", ("ain", 2), List(gizmo), None)

val zombiejs = Json.obj(
  "name" -> "zombie",
  "isDead" -> true,
  "weight" -> 100.0,
  "email" -> "[email protected]",
  "favorites" -> Json.obj(
    "string" -> "ain",
    "number" -> 2
  ),
  "friends" -> Json.arr(
    Json.obj(
      "name" -> "gremlins",
      "isDead" -> false,
      "weight" -> 1.0,
      "email" -> "[email protected]",
      "favorites" -> Json.obj(
        "string" -> "alpha",
        "number" -> 85
      ),
      "friends" -> Json.arr(),
      "social" -> "@gizmo"
    )
  ),
  "social" -> JsNull
)

assert(Json.toJson(zombie) == zombiejs)

// JSON から Creature へのデシリアライズをテストする (非対称的な読み込みであることに注意してください)
val gizmo2 = Creature("gremlins", false, 1.0F, "[email protected]", ("pha", 85), List(), Some("@gizmo"))
val zombie2 = Creature("zombie", true, 100.0F, "[email protected]", ("n", 2), List(gizmo2), None)

assert(Json.fromJson[Creature](zombiejs).get == zombie2)

§Format[A] のその他の機能

§Format[A].inmap( A => B, B => A ): Format[B]

Format[A] は共変でもあり反変でもある (つまり不変な) Functor です。
このため、Format[B] に変換するための関数 A => B と関数 B => A の両方を与えてやらなければなりません。

例えば:

scala> case class Person(name: String)
defined class Person

scala> __.format[String].inmap( (name: String) => Person(name), (p: Person) => p.name )
res6: play.api.libs.json.OFormat[Person] = [email protected]

Next: JSON トランスフォーマー


このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。