§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 結構是「語言原生」,並允許相對於您的業務邏輯操作資料,同時確保業務邏輯與 Web 層的隔離。
- 原因較為可疑:ORM 框架僅使用 OO 結構與資料庫通訊,我們(某種程度上)說服自己不可能做到其他事情…具有 ORM 的眾所周知的優缺點…(不在這裡批評這些東西)
§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。
- 轉換 JSON 以符合預期的資料庫文件結構。
- 直接將 JSON 傳送到資料庫(或其他地方)。
這與從資料庫提供資料時完全相同
- 直接從資料庫中提取一些資料作為 JSON。
- 過濾/轉換此 JSON,僅傳送客戶端預期的格式中的強制性資料(例如,您不希望某些安全資訊外洩)。
- 直接將 JSON 傳送到客戶端。
在此背景下,我們可以輕鬆想像操作 JSON 資料流程,從客戶端到資料庫再返回,而無需在 JSON 以外的任何其他內容中進行任何(明確)轉換。
當然,當您將此轉換流程插入Play2.1 提供的反應式基礎架構時,它突然開啟了新的視野。
這就是所謂的(由我)JSON 端到端設計
- 不要將 JSON 資料視為一塊一塊的,而應視為從客戶端到資料庫(或其他)透過伺服器傳送的連續資料串流
- 將JSON 串流視為可與其他串流連接的管線,同時套用修改和轉換
- 以完全非同步/非封鎖的方式處理串流。
這也是 Play2.1 反應式架構存在的其中一個原因...
我相信透過資料串流的觀點思考你的應用程式會大幅改變你設計一般網路應用程式的思維。它也可能開啟新的功能範圍,比傳統架構更符合今日網路應用程式的需求。不過,這不是這裡的主題 ;)
因此,正如你自行推論的,為了能夠直接根據驗證和轉換來處理 Json 串流,我們需要一些新工具。JSON 組合器是很好的候選者,但它們有點太過通用。
這就是我們建立一些特殊組合器和 API,稱為JSON 轉換器來執行此目的的原因。
§JSON 轉換器為 Reads[T <: JsValue]
- 你可以說 JSON 轉換器只是
f:JSON => JSON
。 - 因此,JSON 轉換器可以僅為
Writes[A <: JsValue]
。 - 但是,JSON 轉換器不只是一個函式:如前所述,我們也希望在轉換 JSON 的同時驗證它。
- 因此,JSON 轉換器為
Reads[A <: 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...
- 所有 JSON 轉換器都位於
JsPath.json.
§(__ \ 'key2 \ 'key23).json.pick
pick
是一個Reads[JsValue]
,它會挑選給定 JsPath 中的值。這裡是["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]
是一個Reads[T <: JsValue]
,它會挑選給定JsPath
中的值(在我們的案例中為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
pickBranch
是一個Reads[JsValue]
,它會挑選從根節點到給定JsPath
的分支
§{"key2":{"key24":{"key242":"value242"}}}
- 結果是從根節點到給定 JsPath 的分支,包括
JsPath
中的 JsValue
提醒
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 讀取 JsValuecopyFrom
將此提取的 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
,並重新建立分支(__ \ 'key2 \ 'key24)
,將reads
的結果新增為葉節點。 - 將此分支與完整的輸入 JSON 合併,取代現有的分支(因此它只適用於輸入
JsObject
,而不適用於其他類型的JsValue
)。
§JsSuccess({…},)
- 僅供參考,那裡沒有 JsPath 作為第 2 個參數,因為 JSON 處理是從 Root 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
- 是
Reads[JsObject]
,僅適用於 JsObject
§(__ \ 'key2 \ 'key22).json.prune
- 從輸入 JSON 中移除指定的 JsPath(
key22
已從key2
下消失)
請注意,產生的 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])
- 從輸入的 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]
的組合。 - 先套用第一個讀取,然後將結果傳遞至第二個讀取。
§of[JsArray].map{ case JsArray(arr) => JsArray(arr :+ JsString("delta")
- 讀取 JsArray(
__ \ 'key23
中的 _值 [alpha, beta, gamma]_)。 - 使用
Reads[A].map
將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])
- 從輸入的 JSON 中萃取分支
__ \ 'key2
,並將reads
套用至該分支的相對葉節點(僅針對內容)。
§(__ \ 'key23).json.prune
- 從相對 JSON 中移除分支
__ \ 'key23
請注意,結果僅為
__ \ '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
- 它會合併兩個
Reads[JsObject]
的結果 (JsObject ++ JsObject) - 它也會將同一個 JSON 套用於兩個
Reads[JsObject]
,這與andThen
不同,後者會將第一個讀取的結果注入到第二個讀取中。
下一頁:使用 XML
在此文件檔中發現錯誤?此頁面的原始程式碼可以在 這裡找到。在閱讀完 文件檔指南 後,請隨時提出拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。