§JSON トランスフォーマー
このドキュメントは、Pascal Voitot (@mandubian) が mandubian.com に投稿した記事として最初に公開されたことにご注意ください。
これで、JSON を検証し、Scala で記述できる任意の構造に変換し、JSON に戻す方法がわかりました。しかし、これらのコンビネーターを使用して Web アプリケーションを書き始めるとすぐに、すぐに次のようなケースに遭遇しました。ネットワークから JSON を読み取り、検証し、… JSON に変換します。
§JSON の海岸間設計の紹介
§JSON を OO に変換する運命にあるのでしょうか?
ここ数年、ほとんどすべての Web フレームワーク(おそらく JSON がデフォルトのデータ構造である最近の JavaScript サーバーサイドのものは除く)で、ネットワークから JSON を取得し、JSON(または POST/GET データ)をクラス(または Scala のケースクラス)などの OO 構造に変換することに慣れています。なぜでしょうか?
- 良い理由があります。OO 構造は「言語ネイティブ」であり、ビジネスロジックを考慮してデータをシームレスに操作しながら、ビジネスロジックと Web レイヤーの分離を保証します。
- より疑問の余地のある理由として、ORM フレームワークは OO 構造でのみ DB と通信するため、(ORM のよく知られた長所と短所と共に…ここではそれらを批判するつもりはありません)…他に方法がないと信じ込まされてきました。
§OO 変換は本当にデフォルトのユースケースですか?
多くの場合、データに対して実際のビジネスロジックを実行する必要はありませんが、保存前または抽出後に検証/変換する必要があります。CRUD のケースを見てみましょう。
- ネットワークからデータを取得し、少し検証してから DB に挿入/更新します。
- 逆に、DB からデータを取得して外部に送信します。
そのため、一般的に CRUD 操作では、フレームワークが OO でしか話すことができないため、JSON を OO 構造に変換します。
JSON から OO への変換を使用すべきではないと言っているわけではありませんが、おそらくこれは最も一般的なケースではなく、実際のビジネスロジックを実行する場合にのみ OO への変換を維持する必要があります。
§新しいテクノロジーのプレーヤーが JSON の操作方法を変えています
この事実以外にも、MongoDB(または CouchDB)などの新しい DB タイプがあり、JSON ツリーのように見えるドキュメント構造のデータを受け入れます(_BSON、バイナリ JSON ではありませんか?_)。
これらの DB タイプでは、ReactiveMongo などの優れた新しいツールもあり、非常に自然な方法で Mongo とのデータの送受信をストリーミングするためのリアクティブ環境を提供します。
Play2-ReactiveMongo モジュール を作成する際に、Stephane Godbillon と協力して Play2.1 に ReactiveMongo を統合しました。このモジュールは、Play2.1 の Mongo 機能に加えて、Json から BSON への変換タイプリファレンスを提供します。
つまり、OO に変換することなく、DB との JSON フローを直接操作できるということです。
§JSON の海岸間設計
これを考慮すると、次のようなものを簡単に想像できます。
- JSON を受信します。
- JSON を検証します。
- 期待される DB ドキュメント構造に合わせて JSON を変換します。
- JSON を DB(または他の場所)に直接送信します。
これは、DB からデータを提供する場合と同じケースです。
- JSON として DB から直接いくつかのデータを取り出します。
- この JSON をフィルタリング/変換して、クライアントが期待する形式で必須データのみを送信します(たとえば、機密情報は外部に出さないようにします)。
- JSON をクライアントに直接送信します。
このコンテキストでは、クライアントから DB(またはその他)へのサーバーを通じたデータの連続フローとしてJSON データのフローを操作することを簡単に想像できます。
Play2.1 のリアクティブインフラストラクチャにこの変換フローを接続すると、突然新しい地平が開けます。
これが、私がそう呼んでいるJSON の海岸間設計です。
- JSON データをチャンク単位で考えるのではなく、クライアントから DB(またはその他)へのサーバーを通じたデータの連続フローとして考えます。
- 変更や変換を適用しながら、JSON フローを他のパイプに接続するパイプとして扱います。
- フローを完全に非同期/ノンブロッキングの方法で扱います。
これも Play2.1 リアクティブアーキテクチャの存在理由の 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)
と同じです。
§詳細
以下のコードサンプルでは、次の JSON を使用します。
{
"key1" : "value1",
"key2" : {
"key21" : 123,
"key22" : true,
"key23" : [ "alpha", "beta", "gamma"],
"key24" : {
"key241" : 234.123,
"key242" : "value242"
}
},
"key3" : 234
}
§ケース 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(s)
を構成するために使用されます。 ["alpha","beta","gamma"]
は、単にtoString
をオーバーライドしたためです。
リマインダー
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)
は3つのことを行います。
- JsPath
(__ \ 'key2 \ 'key24)
で入力JSONから値を抽出します。 - この相対的な値に
reads
を適用し、reads
の結果を葉として追加してブランチ(__ \ 'key2 \ 'key24)
を再作成します。 - 既存のブランチを置き換えることで、このブランチを入力JSON全体とマージします(そのため、入力
JsObject
でのみ機能し、他のタイプのJsValue
では機能しません)。
§JsSuccess({…},)
- 参考までに、JSON操作はルートJsPathから行われたため、2番目のパラメーターとしてJsPathはありません。
リマインダー
jsPath.json.update(Reads[A <: JsValue])
はJsObject
でのみ機能し、入力JsObject
全体をコピーして、提供されたReads[A <: JsValue]
を使用して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
引数を受け取ります。そのため、クロージャも渡すことができます。
§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:ブランチを選択し、その内容を2箇所で更新する
import play.api.libs.json._
import play.api.libs.json.Reads._
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_)を読み取ります。 Reads[A].map
を使用して、(当然ながら不変の方法で)10を増やします。
§andThen
- 2つの
Reads[A]
の合成です。 - 最初に最初のreadsが適用され、次に結果が2番目のreadsにパイプされます。
§of[JsArray].map{ case JsArray(arr) => JsArray(arr :+ JsString("delta")
- JsArray(
__ \ 'key23
内の_値[alpha, beta, gamma]_)を読み取ります。 Reads[A].map
を使用して、JsString("delta")
を追加します。
ブランチ
__ \ 'key2
のみを選択したため、結果はブランチ__ \ '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トランスフォーマーを作成するための膨大なツールキットが手に入ったことを覚えておいてください。トランスフォーマーを組み合わせて、他のトランスフォーマーにマップしたり、フラットマップしたりすることができます。そのため、可能性はほぼ無限です。
しかし、最後に1点だけ対処する必要があります。これらの新しい優れたJSONトランスフォーマーと、先に紹介したReadsコンビネーターを組み合わせることです。JSONトランスフォーマーは単なるReads[A <: JsValue]
なので、これは非常に簡単です。
Gizmo to Gremlin JSONトランスフォーマーを作成することで説明します。
こちらがGizmoです。
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"
)
こちらがGremlinです。
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.json.Reads._
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の結果を2番目のreadsに挿入する
andThen
とは異なり、両方のReads[JsObject]
に同じJSONを適用します。
次:XMLの操作
このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送信して自由に貢献してください。質問やアドバイスを共有したいですか?コミュニティフォーラムにアクセスして、コミュニティとの会話を始めましょう。