Documentation

§JSON トランスフォーマー

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

ここまでで、JSON のバリデーションを行い、Scala で記述できるあらゆる構造に変換し、そして JSON に書き戻す方法は分かったはずです。しかし、これらのコンビネータを使って web アプリケーションを書き始めたとき、ほとんどすぐに私はあるケースに遭遇しました : ネットワークから JSON を読み込み、バリデーションを行い、そしてそれを変換するのです… JSON に。

§端から端への JSON 設計の紹介

§JSON からオブジェクト指向への変換は絶望的?

ここ数年、(JSON をデフォルトのデータ構造とする最近のサーバサイド JavaScript フレームワークを除く) ほとんどすべてのフレームワークにおいて、ネットワークから JSON を取得し、 そして JSON を (あるいは POST/GET データさえも) クラス (または Scala ではケースクラス) のようなオブジェクト指向構造に変換 してきました。なぜでしょう?

§オブジェクト指向への変換は本当にデフォルトのユースケースか?

ほとんどの場合、データと共に本物のビジネスロジックを実行する必要は無く、データを格納する前や展開した後で、バリデーション/変換を行う必要があるだけです。CRUD の場合を取り上げてみましょう:

このように、一般的に CRUD 操作において JSON をオブジェクト指向構造に変換するのは、フレームワークがオブジェクト指向的にしか会話しないためです。

JSON からオブジェクト指向構造への変換を使うべきではないと言ったり、取り繕ったりするわけではありませんが、多くの場合においてオブジェクト指向構造への変換は必要はなく、満たすべき本物のビジネスロジックが存在するときのみに止めるべきです。

§新たな技術者達が JSON の操作方法を変える

事実、MongoDB (または 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 のことを考慮すると、次のようなことをぱっと思い付きます:

これは DB からデータを提供するときとまったく同じです:

この文脈において、JSON ではない何かへのあらゆる (明示的な) 変換を伴わずに、クライアントから DB へ、そしてその逆方向へ JSON データフローを操作 できたらと容易に想像します。
この変換フローを Play2.1 が提供するリアクティブなインフラ にごく自然に当てはめたとき、新たな地平線が突然開けます。

このため、これは 端から端への JSON 設計 と (私によって) 呼ばれています:

  • JSON データは、その都度ごとのチャンクと捉えるのではなく、 サーバーを経由したクライアントから DB (またはその他) への継続的なデータフロー と考えてください
  • 変更や変換を適用している間、並行して JSON フローを、その他のパイプにつなぐパイプ として取り扱ってください
  • フローは 完全に非同期/ノンブロッキング な方法で取り扱ってください

これもまた、Play2.1 がリアクティブなアーキテクチャである理由のひとつです…
データフローのプリズムを通じてアプリケーションを考慮することは、概して web アプリケーションの設計方法を劇的に変化させる と信じています。これまでのアーキテクチャよりもずっと良く今日の web アプリケーションの要件にフィットする機能的なスコープを切り開くかもしれません。まあこれはここで話すべきことではありません ;)

このため、あなたがご自身で推論したように、バリデーションおよび変換に基づいた Json フローを直接操作できるようにするために、新しいツールが必要です。JSON コンビネータは良い候補でしたが、少し総称的過ぎます。これが、これを行う JSON トランスフォーマー と呼ばれる特別なコンビネータと API を作った理由です。

§JSON トランスフォーマーは Reads[T <: 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...

§(__ \ 'key2 \ 'key23).json.pick

§JsSuccess(["alpha","beta","gamma"],/key2/key23)

リマインダー
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]

リマインダー
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

§{"key2":{"key24":{"key242":"value242"}}}

リマインダー
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] )

§{"key25":{"key251":123}}

リマインダー
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])

§(__ \ 'key2 \ 'key24).json.update(reads) は三つのことを行います:

§JsSuccess({…},)

リマインダー
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 )

§(__ \ 'key24 \ 'key241).json.put( a: => JsValue )

§jsPath.json.put( a: => JsValue )

§jsPath.json.put

リマインダー
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

§(__ \ 'key2 \ 'key22).json.prune

結果の JsObject のキーの並びが入力された JsObject のものと同じでないことに注意してください。これは JsObject の実装とマージ機構によるものです。しかし、これを考慮に入れて JsObject.equals をオーバーライドしているので、このことは重要ではありません。

リマインダー
jsPath.json.prune は JsObject に対してのみ動作し、入力された JSON から与えられた JsPath を削除します。

以下に注意してください:
- prune は今のところ JsPath に対して再帰的には動作しません
- prune が削除するブランチを見つけなかった場合、一切のエラーを生成せず、変更されていない JSON を返します。

§より複雑なケース

§ケース 7: 二箇所にあるブランチの内容の取得と更新

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])

§(__ \ 'key21).json.update(reads: Reads[A <: JsValue])

§of[JsNumber]

§of[JsNumber].map{ case JsNumber(nb) => JsNumber(nb + 10) }

§andThen

§of[JsArray].map{ case JsArray(arr) => JsArray(arr :+ JsString("delta")

__ \ '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])

§(__ \ 'key23).json.prune

この結果が、単に 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.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

Next: JSON Macro Inception


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