§JSON トランスフォーマー
このドキュメントは、当初 Pascal Voitot (@mandubian) の記事 mandubian.com として公開されたものです
ここまでで、JSON のバリデーションを行い、Scala で記述できるあらゆる構造に変換し、そして JSON に書き戻す方法は分かったはずです。しかし、これらのコンビネータを使って web アプリケーションを書き始めたとき、ほとんどすぐに私はあるケースに遭遇しました : ネットワークから JSON を読み込み、バリデーションを行い、そしてそれを変換するのです… JSON に。
§ 端から端への JSON 設計の紹介
§JSON からオブジェクト指向への変換は絶望的?
ここ数年、(JSON をデフォルトのデータ構造とする最近のサーバサイド JS フレームワークを除く) ほとんどすべてのフレームワークにおいて、ネットワークから JSON を取得し、 そして JSON を (あるいは POST/GET データさえも) クラス (または Scala ではケースクラス) のようなオブジェクト指向構造に変換 してきました。なぜでしょう?
- 妥当な理由 : オブジェクト指向構造は “プログラミング言語ネイティブ” であり、ビジネスロジックを web 層から隔離しつつ、自然な方法で ビジネスロジックに対してデータを操作する ことができるため
- より疑わしい理由 : ORM フレームワークはオブジェクト指向構造を通してしか DB とやり取りできない、他のやり方なんて不可能だ …ORM のよく知られた良い機能や悪い機能と付き合いながらね…と自分たちを (ある程度) 納得させているため (ここで ORM を批判しているわけではありません)
§オブジェクト指向への変換は本当にデフォルトのユースケースか?
ほとんどの場合、データと共に本物のビジネスロジックを実行する必要は無く、データを格納する前や展開した後で、バリデーション/変換を行う必要があるだけです。
CRUD の場合を取り上げてみましょう:
- ネットワークからデータをただ取得し、ちょろっとバリデーションして DB にinsert/update する
- 逆に、DB からデータをただ検索し、それらを外部に送信する
このように、一般的に CRUD 操作において JSON をオブジェクト指向構造に変換するのは、フレームワークがオブジェクト指向的にしか会話しないためです。
JSON からオブジェクト指向構造への変換を使うべきではないと言ったり、取り繕ったりするわけではありませんが、多くの場合においてオブジェクト指向構造への変換は必要はなく、満たすべき本物のビジネスロジックが存在するときのみに止めるべきです。
§新たな技術者達が JSON の操作方法を変える
事実、Mongo (または CouchDB) のような、ほとんど JSON (_BSON, つまり Binary JSON ですよね?_) ツリーのように見えるドキュメント構造のデータを受け入れる新しいタイプの DB があります。
このようなタイプの DB のために、とても自然なやり方で Mongo とストリームデータをやり取りすることのできる、リアクティブな環境を提供する ReactiveMongo のような、新しくて素晴らしいツールもあります。
私は Play2-ReactiveMongo module を書きながら、Play2.1 と共に ReactiveMongo を実装するために Stephane Godbillon と共に働きました。Play2.1 における Mongo の容易さに加えて、このモジュールは Json から BSON に、または BSON から Json に変換する型クラス を提供します。
これは、DB に対する JSON データフローのやり取りを、オブジェクト指向構造に変換することなく直接操作できることを意味しています。
§ 端から端への JSON 設計
JSON のことを考慮すると、次のようなことをぱっと思い付きます:
- JSON を受け取り、
- JSON のバリデーションを行い、
- DB のドキュメント構造に合うように JSON を変換して、
- JSON を直接 DB (またはその他のどこか) に送る
これは DB からデータを提供するときとまったく同じです:
- DB からいくつかのデータを JSON として直接抽出して、
- クライアントが期待するフォーマット (例えば、セキュリティに関する情報は外部に送信したくありません) にて要求されたデータのみを送信するために JSON をフィルタリング/変換して、
- クライアントに JSON を直接送信する
この文脈において、JSON ではない何かへのあらゆる (明示的な) 変換を伴わずに、クライアントから DB へ、そしてその逆方向へ JSON データフローを操作 できたらと容易に想像します。
この変換フローを Play2.1 が提供するリアクティブなインフラ にごく自然に当てはめたとき、新たな地平線が突然開けます。
このため、これは 端から端への JSON 設計 と (私によって) 呼ばれています:
- JSON データは、その都度ごとのチャンクと捉えるのではなく、 サーバーを経由したクライアントから DB (またはその他) への継続的なデータフロー と考えてください
- 変更や変換を適用している間、並行して JSON フローを、その他のパイプにつなぐパイプ として取り扱ってください
- フローは 完全に非同期/ノンブロッキング な方法で取り扱ってください
これもまた、Play2.1 がリアクティブなアーキテクチャである理由のひとつです…
データフローのプリズムを通じてアプリケーションを考慮することは、概して web アプリケーションの設計方法を劇的に変化させる と信じています。これまでのアーキテクチャよりもずっと良く今日の web アプリケーションの要件にフィットする機能的なスコープを切り開くかもしれません。まあこれはここで話すべきことではありません ;)
このため、あなたがご自身で推論したように、バリデーションおよび変換に基づいた Json フローを直接操作できるようにするために、新しいツールが必要です。JSON コンビネータは良い候補でしたが、少し総称的過ぎます。これが、これを行う JSON トランスフォーマー と呼ばれる特別なコンビネータと API を作った理由です。
§JSON トランスフォーマーは Reads[T <: JsValue]
JSON トランスフォーマーは単なる f:JSON => JSON
であると言うかもしれません。
このため、JSON トランスフォーマーは Writes[A <: JsValue]
を簡単にすることができるだろうと。
しかし、JSON トランスフォーマーは関数だけではありません: すでに述べたように、JSON を変換している間にバリデーションも行いたいと考えています。
結論として、JSON トランスフォーマーは Reads[A <: Jsvalue]
なのです。
Reads[A <: JsValue] は、読み込み/バリデーションするだけではなく、変換もできることを忘れないでください
§JsValue.validate
の代わりに JsValue.transform
を使う
人々が Reads[T]
を単なるバリデータではなく、トランスフォーマーと見なしやすくするために、JsValue
にヘルパー関数を提供しました:
JsValue.transform[A <: JsValue](reads: Reads[A]): JsResult[A]
これは JsValue.validate(reads)
とまったく同じです
§ケース 1: JsPath にある JSON を取り出す
§JsValue として値を取り出す
import play.api.libs.json._
val jsonTransformer = (__ \ 'key2 \ 'key23).json.pick
scala> json.transform(jsonTransformer)
res9: play.api.libs.json.JsResult[play.api.libs.json.JsValue] =
JsSuccess(
["alpha","beta","gamma"],
/key2/key23
)
§(__ \ 'key2 \ 'key23).json...
- すべての JSON トランスフォーマーは
JsPath.json.
に存在します。
§(__ \ 'key2 \ 'key23).json.pick
pick
は、与えられた JsPath に 含まれる 値を取り出すReads[JsValue]
です。この場合は["alpha","beta","gamma"]
です。
§JsSuccess(["alpha","beta","gamma"],/key2/key23)
- すんなりと成功した
JsResult
です - ちなみに、
/key2/key23
はデータが読みだされた JsPath を表現しますが、これは主に Play API が JsResult を組み上げるために利用するものなので、気にしないでください。 - 単に
toString
をオーバーライドしたために["alpha","beta","gamma"]
となっています
リマインダー
jsPath.json.pick
は JsPath に含まれる値 だけ を取得します
§値を型として取り出す
import play.api.libs.json._
val jsonTransformer = (__ \ 'key2 \ 'key23).json.pick[JsArray]
scala> json.transform(jsonTransformer)
res10: play.api.libs.json.JsResult[play.api.libs.json.JsArray] =
JsSuccess(
["alpha","beta","gamma"],
/key2/key23
)
§(__ \ 'key2 \ 'key23).json.pick[JsArray]
pick[T]
は、与えられた JsPath に 含まれる 値を (この場合はJsArray
として) 取り出すReads[T <: JsValue]
です。
リマインダー
jsPath.json.pick[T <: JsValue]
は JsPath に含まれる型付けされた値 だけ を取得します
§ケース 2: JsPath に従ってブランチを取り出す
§ブランチを JsValue として取り出す
import play.api.libs.json._
val jsonTransformer = (__ \ 'key2 \ 'key24 \ 'key241).json.pickBranch
scala> json.transform(jsonTransformer)
res11: play.api.libs.json.JsResult[play.api.libs.json.JsObject] =
JsSuccess(
{
"key2": {
"key24":{
"key241":234.123
}
}
},
/key2/key24/key241
)
§(__ \ 'key2 \ 'key23).json.pickBranch
pickBranch
は、ルートから与えらえた JsPath までのブランチを取り出すReads[JsValue]
です
§{"key2":{"key24":{"key242":"value242"}}}
- この結果は、JsPath 内の JsValue を含む、ルートから与えられた JsPath までのブランチです
リマインダー
jsPath.json.pickBranch
は、JsPath まで掘り下げていくひとつのブランチと、JsPath に含まれる値を展開します
§ケース 3: 入力の JsPath から新しい JsPath に値をコピーする
import play.api.libs.json._
val jsonTransformer = (__ \ 'key25 \ 'key251).json.copyFrom( (__ \ 'key2 \ 'key21).json.pick )
scala> json.transform(jsonTransformer)
res12: play.api.libs.json.JsResult[play.api.libs.json.JsObject]
JsSuccess(
{
"key25":{
"key251":123
}
},
/key2/key21
)
§(__ \ 'key25 \ 'key251).json.copyFrom( reads: Reads[A <: JsValue] )
copyFrom
はReads[JsValue]
ですcopyFrom
は、提供されている Reads[A] を使って入力された JSON から JsValue を読み込みますcopyFrom
は、抽出された JsValue を与えられた JsPath に対応する新しいブランチのリーフとしてコピーします
§{"key25":{"key251":123}}
copyFrom
は、値123
を読み込みますcopyFrom
は、この値を新しいブランチ(__ \ 'key25 \ 'key251)
にコピーします
リマインダー
jsPath.json.copyFrom(Reads[A <: JsValue])
は、入力された JSON から値を読み出し、この結果をリーフとして新しいブランチを作ります
§ケース 4: 入力された JSON 全体のコピーとブランチの更新
import play.api.libs.json._
val jsonTransformer = (__ \ 'key2 \ 'key24).json.update(
__.read[JsObject].map{ o => o ++ Json.obj( "field243" -> "coucou" ) }
)
scala> json.transform(jsonTransformer)
res13: play.api.libs.json.JsResult[play.api.libs.json.JsObject] =
JsSuccess(
{
"key1":"value1",
"key2":{
"key21":123,
"key22":true,
"key23":["alpha","beta","gamma"],
"key24":{
"key241":234.123,
"key242":"value242",
"field243":"coucou"
}
},
"key3":234
},
)
§(__ \ 'key2).json.update(reads: Reads[A < JsValue])
- これは
Reads[JsObject]
です
§(__ \ 'key2 \ 'key24).json.update(reads)
は三つのことを行います:
- 入力された JSON の JsPath
(__ \ 'key2 \ 'key24)
にある値を抽出します - この関連した値に
reads
を適用し、このreads
の結果を再作成した新しいブランチ(__ \ 'key2 \ 'key24)
にリーフとして追加します - 既存のブランチを置き換えながら、入力された JSON 全体にこの新しいブランチをマージします (このため、これは JsObject でのみ動作し、その他の種類の JsValue では動作しません)
§JsSuccess({…},)
- 参考までに、ここでの JSON 操作はルートの JsPath で行われるため、第二引数としての JsPath は存在しません
リマインダー
jsPath.json.update(Reads[A <: JsValue])
は JsObject に対してのみ動作し、提供されているReads[A <: JsValue]
で入力されたJsObject
全体をコピーして、JsPath を更新します
§ケース 5: 新しいブランチに与えられた値を設定する
import play.api.libs.json._
val jsonTransformer = (__ \ 'key24 \ 'key241).json.put(JsNumber(456))
scala> json.transform(jsonTransformer)
res14: play.api.libs.json.JsResult[play.api.libs.json.JsObject] =
JsSuccess(
{
"key24":{
"key241":456
}
},
)
§(__ \ 'key24 \ 'key241).json.put( a: => JsValue )
- これは Reads[JsObject] です
§(__ \ 'key24 \ 'key241).json.put( a: => JsValue )
- 新しいブランチ
(__ \ 'key24 \ 'key241)
を作成します a
をこのブランチのリーフとして追加します
§jsPath.json.put( a: => JsValue )
- 名前で渡された JsValue 引数を取り、JsValue に対するクロージャでさえも渡すことができます。
§jsPath.json.put
- 入力された JSON についてまったく気にしません
- 与えられた値で入力された JSON をシンプルに置き換えます
リマインダー
jsPath.json.put( a: => Jsvalue )
は、入力の JSON について考慮することなしに、与えられた値で新しいブランチを作成します
§ケース 6: 入力 された JSON からブランチを取り除く
import play.api.libs.json._
val jsonTransformer = (__ \ 'key2 \ 'key22).json.prune
scala> json.transform(jsonTransformer)
res15: play.api.libs.json.JsResult[play.api.libs.json.JsObject] =
JsSuccess(
{
"key1":"value1",
"key3":234,
"key2":{
"key21":123,
"key23":["alpha","beta","gamma"],
"key24":{
"key241":234.123,
"key242":"value242"
}
}
},
/key2/key22/key22
)
§(__ \ 'key2 \ 'key22).json.prune
- これは JsObject に対してのみ動作する
Reads[JsObject]
です
§(__ \ 'key2 \ 'key22).json.prune
- 入力された JSON から与えられた JsPath を削除します (
key2
からkey22
が消えています)
結果の JsObject のキーの並びが入力された JsObject のものと同じでないことに注意してください。これは JsObject の実装とマージ機構によるものです。しかし、これを考慮に入れて JsObject.equals
をオーバーライドしているので、このことは重要ではありません。
リマインダー
jsPath.json.prune
は JsObject に対してのみ動作し、入力された JSON から与えられた JsPath を削除します。以下に注意してください:
-prune
は今のところ JsPath に対して再帰的には動作しません
-prune
が削除するブランチを見つけなかった場合、一切のエラーを生成せず、変更されていない JSON を返します。
§より複雑なケース
§ケース 7: 二箇所にあるブランチの内容の取得と更新
import play.api.libs.json._
val jsonTransformer = (__ \ 'key2).json.pickBranch(
(__ \ 'key21).json.update(
of[JsNumber].map{ case JsNumber(nb) => JsNumber(nb + 10) }
) andThen
(__ \ 'key23).json.update(
of[JsArray].map{ case JsArray(arr) => JsArray(arr :+ JsString("delta")) }
)
)
scala> json.transform(jsonTransformer)
res16: play.api.libs.json.JsResult[play.api.libs.json.JsObject] =
JsSuccess(
{
"key2":{
"key21":133,
"key22":true,
"key23":["alpha","beta","gamma","delta"],
"key24":{
"key241":234.123,
"key242":"value242"
}
}
},
/key2
)
§(__ \ 'key2).json.pickBranch(reads: Reads[A <: JsValue])
- 入力された JSON から
__ \ 'key2
ブランチを抽出し、このブランチの関連するリーフにreads
(の内容のみ) を適用します
§(__ \ 'key21).json.update(reads: Reads[A <: JsValue])
(__ \ 'key21)
ブランチを更新します
§of[JsNumber]
- これは、ただの
Reads[JsNumber]
です (__ \ 'key21)
から JsNumber を抽出します
§of[JsNumber].map{ case JsNumber(nb) => JsNumber(nb + 10) }
- JsNumber (
__ \ 'key21
にある 値 123) を読み込みます - 値を 10 だけ (自然にイミュータブルな方法で) 増やすために
Reads[A].map
を使います
§andThen
- これは、単に二つの
Reads[A]
を合成します - ひとつめの reads が適用され、それからその結果が二つ目の reads にパイプされます
§of[JsArray].map{ case JsArray(arr) => JsArray(arr :+ JsString("delta")
- JsArray (_
__ \ 'key23
にある値 [alpha, beta, gamma]_) を読み込みます - これに
JsString("delta")
を追加するためにReads[A].map
を使います
__ \ 'key2
ブランチだけを取り出したので、結果もこのブランチだけになることに注意してください
§ケース 8: ブランチを取り出してサブブランチを取り除く
import play.api.libs.json._
val jsonTransformer = (__ \ 'key2).json.pickBranch(
(__ \ 'key23).json.prune
)
scala> json.transform(jsonTransformer)
res18: play.api.libs.json.JsResult[play.api.libs.json.JsObject] =
JsSuccess(
{
"key2":{
"key21":123,
"key22":true,
"key24":{
"key241":234.123,
"key242":"value242"
}
}
},
/key2/key23
)
§(__ \ 'key2).json.pickBranch(reads: Reads[A <: JsValue])
- 入力された JSON から
__ \ 'key2
ブランチを抽出し、このブランチの関連するリーフにreads
(の内容のみ) を適用します
§(__ \ 'key23).json.prune
- 関連する JSON から
__ \ 'key23
ブランチを削除します
この結果が、単に
key23
フィールドのない__ \ 'key2
ブランチであることに注目してください
§コンビネータは?
退屈する前にやめておきます (もしまだ退屈していないなら、ですけど) …
今や汎用的な JSON トランスフォーマーを作り出すたくさんのツールキットを手に入れたことを覚えておいてください。
トランスフォーマーを他のトランスフォーマーに合成し、はめ込み、展開することができます。そう、可能性はほとんど無限です。
ただし、最後に気を付けなければならない点が一点あります: これらの新しくてすごい JSON トランスフォーマーと、以前に存在していた Reads コンビネータを混ぜる場合です。
JSON トランスフォーマーは単なる Reads[A <: JsValue]
なので、これはまったく些細なことです。
ギズモからグレムリン JSON トランスフォーマーを書いて実証しましょう。
これがギズモです:
val gizmo = Json.obj(
"name" -> "gizmo",
"description" -> Json.obj(
"features" -> Json.arr( "hairy", "cute", "gentle"),
"size" -> 10,
"sex" -> "undefined",
"life_expectancy" -> "very old",
"danger" -> Json.obj(
"wet" -> "multiplies",
"feed after midnight" -> "becomes gremlin"
)
),
"loves" -> "all"
)
これがグレムリンです:
val gremlin = Json.obj(
"name" -> "gremlin",
"description" -> Json.obj(
"features" -> Json.arr("skinny", "ugly", "evil"),
"size" -> 30,
"sex" -> "undefined",
"life_expectancy" -> "very old",
"danger" -> "always"
),
"hates" -> "all"
)
それでは、この変換を行う JSON トランスフォーマーを書いてみましょう
import play.api.libs.json._
import play.api.libs.functional.syntax._
val gizmo2gremlin = (
(__ \ 'name).json.put(JsString("gremlin")) and
(__ \ 'description).json.pickBranch(
(__ \ 'size).json.update( of[JsNumber].map{ case JsNumber(size) => JsNumber(size * 3) } ) and
(__ \ 'features).json.put( Json.arr("skinny", "ugly", "evil") ) and
(__ \ 'danger).json.put(JsString("always"))
reduce
) and
(__ \ 'hates).json.copyFrom( (__ \ 'loves).json.pick )
) reduce
scala> gizmo.transform(gizmo2gremlin)
res22: play.api.libs.json.JsResult[play.api.libs.json.JsObject] =
JsSuccess(
{
"name":"gremlin",
"description":{
"features":["skinny","ugly","evil"],
"size":30,
"sex":"undefined",
"life_expectancy":
"very old","danger":"always"
},
"hates":"all"
},
)
はい、できた ;)
もう理解できるはずなので、これについてはまったく説明しないことにします。
以下に気を付けてください:
§(__ \ 'features).json.put(…)
は (__ \ 'size).json.update
の後にあるので、オリジナルの (__ \ 'features)
を上書きします
§(Reads[JsObject] and Reads[JsObject]) reduce
- 両方の
Reads[JsObject]
(JsObject ++ JsObject) の結果をマージします - ひとつ目の reads をふたつ目の reads に挿入する
andThen
とは違い、両方のReads[JsObject]
に同じ JSON を適用します
Next: Json マクロ入門
このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。