文件

§JSON 轉換器

請注意,此文件最初由 Pascal Voitot (@mandubian) 以文章形式發布在 mandubian.com

現在您應該知道如何驗證 JSON 並將其轉換成任何您可以在 Scala 中編寫的結構,然後再轉換回 JSON。但是,在我開始使用這些組合器來編寫 Web 應用程式後,我幾乎立即就遇到一個案例:從網路讀取 JSON、驗證它並將其轉換成…JSON。

§介紹 JSON 端對端 設計

§我們是否注定要將 JSON 轉換成 OO?

幾年來,在幾乎所有 Web 框架中(也許除了最近的 JavaScript 伺服器端程式,其中 JSON 是預設資料結構),我們都習慣從網路取得 JSON,並將 JSON(甚至是 POST/GET 資料)轉換成 OO 結構,例如類別(或 Scala 中的案例類別)。為什麼?

§OO 轉換真的是預設使用案例嗎?

在許多情況下,您實際上不需要對資料執行任何實際的業務邏輯,而是在儲存或提取後進行驗證/轉換。讓我們來看 CRUD 案例

所以,一般來說,對於 CRUD 作業,您會將 JSON 轉換成 OO 結構,僅是因為框架只能使用 OO 語言。

我並非說或假裝您不應該使用 JSON 轉換成 OO,但這可能不是最常見的情況,而且我們應該只在有真正的商業邏輯需要滿足時才將其轉換成 OO。

§新的技術參與者改變了 JSON 的操作方式

除了這個事實之外,我們有一些新的資料庫類型,例如 MongoDB(或 CouchDB),它們接受的文件結構化資料看起來幾乎像 JSON 樹(_不是 BSON,二進制 JSON 嗎?_)。

有了這些資料庫類型,我們還有一些很棒的新工具,例如 ReactiveMongo,它提供反應式環境,可以非常自然地將資料串流傳輸到 Mongo 和從 Mongo 傳輸資料。

我在撰寫 Play2-ReactiveMongo 模組 時,一直與 Stephane Godbillon 合作,將 ReactiveMongo 整合到 Play2.1。除了 Play2.1 的 Mongo 設施之外,這個模組還提供Json 轉換成 BSON 轉換類型類別

因此,這表示您可以直接操作資料庫的 JSON 流程,而無需轉換成 OO。

§JSON 端到端設計

考慮到這一點,我們可以輕鬆想像以下情況

這與從資料庫提供資料時完全相同

在此背景下,我們可以輕鬆想像操作 JSON 資料流程,從客戶端到資料庫再返回,而無需在 JSON 以外的任何其他內容中進行任何(明確)轉換。
當然,當您將此轉換流程插入Play2.1 提供的反應式基礎架構時,它突然開啟了新的視野。

這就是所謂的(由我)JSON 端到端設計

  • 不要將 JSON 資料視為一塊一塊的,而應視為從客戶端到資料庫(或其他)透過伺服器傳送的連續資料串流
  • JSON 串流視為可與其他串流連接的管線,同時套用修改和轉換
  • 完全非同步/非封鎖的方式處理串流。

這也是 Play2.1 反應式架構存在的其中一個原因...
我相信透過資料串流的觀點思考你的應用程式會大幅改變你設計一般網路應用程式的思維。它也可能開啟新的功能範圍,比傳統架構更符合今日網路應用程式的需求。不過,這不是這裡的主題 ;)

因此,正如你自行推論的,為了能夠直接根據驗證和轉換來處理 Json 串流,我們需要一些新工具。JSON 組合器是很好的候選者,但它們有點太過通用。
這就是我們建立一些特殊組合器和 API,稱為JSON 轉換器來執行此目的的原因。

§JSON 轉換器為 Reads[T <: JsValue]

請記住,Reads[A <: JsValue] 能夠轉換,而不仅仅是讀取/驗證

§使用 JsValue.transform 取代 JsValue.validate

我們在 JsValue 中提供了一個函式輔助程式,以協助人們將 Reads[T] 視為轉換器,而不仅仅是驗證器

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.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

請注意,結果僅為 __ \ 'key2 分支,不含 key23 欄位。

§組合器呢?

在變無聊之前,我就在這裡打住了(如果還沒有的話)…

請記住,您現在有一個用於建立通用 JSON 轉換器的龐大工具組。您可以將轉換器組合、對應、扁平化對應成其他轉換器。因此,可能性幾乎是無限的。

但有一個最後要處理的重點:將這些出色的新 JSON 轉換器與先前提供的 Reads 組合器混合。這相當簡單,因為 JSON 轉換器只是 Reads[A <: JsValue]

讓我們透過撰寫一個 Gizmo 到 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] 和 Reads[JsObject]) reduce

下一頁:使用 XML


在此文件檔中發現錯誤?此頁面的原始程式碼可以在 這裡找到。在閱讀完 文件檔指南 後,請隨時提出拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。