ドキュメント

§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 変換は本当にデフォルトのユースケースですか?

多くの場合、データに対して実際のビジネスロジックを実行する必要はありませんが、保存前または抽出後に検証/変換する必要があります。CRUD のケースを見てみましょう。

そのため、一般的に 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 の海岸間設計

これを考慮すると、次のようなものを簡単に想像できます。

これは、DB からデータを提供する場合と同じケースです。

このコンテキストでは、クライアントから DB(またはその他)へのサーバーを通じたデータの連続フローとしてJSON データのフローを操作することを簡単に想像できます。
Play2.1 のリアクティブインフラストラクチャにこの変換フローを接続すると、突然新しい地平が開けます。

これが、私がそう呼んでいるJSON の海岸間設計です。

  • JSON データをチャンク単位で考えるのではなく、クライアントから DB(またはその他)へのサーバーを通じたデータの連続フローとして考えます。
  • 変更や変換を適用しながら、JSON フローを他のパイプに接続するパイプとして扱います。
  • フローを完全に非同期/ノンブロッキングの方法で扱います。

これも Play2.1 リアクティブアーキテクチャの存在理由の 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)は3つのことを行います。

§JsSuccess({…},)

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

§(__ \ '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.pruneJsObjectでのみ機能し、入力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])

§(__ \ '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のみを選択したため、結果はブランチ__ \ '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トランスフォーマーを作成するための膨大なツールキットが手に入ったことを覚えておいてください。トランスフォーマーを組み合わせて、他のトランスフォーマーにマップしたり、フラットマップしたりすることができます。そのため、可能性はほぼ無限です。

しかし、最後に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

次:XMLの操作


このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送信して自由に貢献してください。質問やアドバイスを共有したいですか?コミュニティフォーラムにアクセスして、コミュニティとの会話を始めましょう。