入門閱讀時間 3 分鐘

DynamoDB JSON 與 Marshalling

你頭一次從 DynamoDB API 讀到原始資料時,它看起來不像你放進去的那個 JSON。一個普通物件 {"status": "open", "priority": 3} 回來時成了 {"status": {"S": "open"}, "priority": {"N": "3"}}。每個值都被裹進一個單鍵物件裡,標明它的型別。那層包裹就是 DynamoDB JSON,而在它和普通 JSON 之間來回轉換,就叫 marshalling

它不是噪聲——它是 DynamoDB 在傳輸線上保持型別無歧義的方式。但它會絆倒任何期待普通 JSON 的人,而且手寫它很容易出錯。

什麼是 DynamoDB JSON?

DynamoDB JSON 是 DynamoDB 使用的、帶型別標籤的傳輸格式,其中每個值都被裹進一個標明其型別的單鍵物件裡——字串是 {"S": "open"},數字是 {"N": "3"}。把普通 JSON 轉成它(以及轉回來)就叫 marshalling。它讓型別無歧義,因為普通 JSON 表達不了集合或二進位制,而且由於 DynamoDB 的數字在傳輸線上以字串形式傳送,一個不帶標籤的 3 會有歧義。

  • DynamoDB JSON 給每個值都標上它的型別——字串是 {"S": "..."},數字是 {"N": "..."},依此類推。
  • Marshalling = 普通 JSON → DynamoDB JSON。Unmarshalling = 反過來。
  • 數字在傳輸線上是字串——是 {"N": "3"},不是 {"N": 3}——為的是保留精度。
  • 這些型別標籤就是那套資料型別系統,你早已用它建模:S、N、B、BOOL、NULL、L、M、SS、NS、BS。
  • 別手寫它。SDK 的文件用戶端(或一個轉換器)替你 marshal;只在除錯或構建運算式時才手工做。

問題所在:普通 JSON 不夠用

JSON 恰好只有三種標量型別——字串、數字、布林——外加 null、陣列和物件。DynamoDB 有更多:二進位制,以及三種 集合 型別(字串集合、數字集合、二進位制集合)——這些 JSON 壓根表達不了。而且因為 DynamoDB 的數字在傳輸線上以字串形式傳送,一個不帶標籤的 3 會有歧義——再加上 JSON 分不清列表和集合。

所以 DynamoDB 沒法直接原樣存你的 JSON——它需要每個值的確切型別被明確寫出。型別描述符就是它做到這一點的方式,無損地,在每一次請求和響應上。

編碼如何工作

每個屬性值都變成一個單鍵物件,它的鍵是一個型別描述符

描述符型別示例
S字串{"S": "open"}
N數字(作為字串){"N": "3"}
B二進位制{"B": "dGV4dA=="}
BOOL布林{"BOOL": true}
NULL空值{"NULL": true}
L列表{"L": [{"S": "a"}, {"N": "1"}]}
M對映{"M": {"k": {"S": "v"}}}
SS / NS / BS字串 / 數字 / 二進位制集合{"SS": ["a", "b"]}

列表和對映把同樣的描述符一路巢狀到底,所以一個深層結構化的項就變成深層被包裹的。數字在傳輸線上是字串是有意為之——這讓 DynamoDB 能保留它完整的 38 位數字精度,而一個 JSON 數字(一個 IEEE-754 雙精度,約 15–17 位有效數字)會悄悄把它舍入掉。這些就是你用來建模的那套資料型別;DynamoDB JSON 只是它們在傳輸線上明確的形式,定義於 AWS 底層 API 參考

例項:一條審計日誌條目

你會在應用裡寫的普通 JSON:

{
  "actor": "u-204",
  "action": "ticket.close",
  "ticketId": 8842,
  "tags": ["billing", "urgent"],
  "redacted": false
}

為 API marshal 成 DynamoDB JSON:

{
  "actor": {"S": "u-204"},
  "action": {"S": "ticket.close"},
  "ticketId": {"N": "8842"},
  "tags": {"SS": ["billing", "urgent"]},
  "redacted": {"BOOL": false}
}

注意這個項背後的選擇:ticketId 變成了帶字串值的 Ntags 作為一個字串集合SS)而非列表,是一個手工的建模選擇——一個吃普通 JSON 的通用轉換器會發出 L,因為 JSON 陣列是有序且可重複的,而 SS 會去重且無序。tags 到底該是 SS 還是 L,是一個轉換器替你做不了的建模決定,而這恰恰是為什麼理解這套編碼很重要。

在 DynoTable 裡轉換

你很少需要手工讀寫這個。把普通 JSON 粘進 DynamoDB JSON 轉換器 來 marshal 它(以及轉回來),而當你在組裝一個請求時,DynamoDB 運算式構建器 會在運算式旁邊發出正確 marshal 後的屬性值對映。在應用本身裡,DynoTable 把項顯示為普通、可讀的值,並在寫入時替你 marshal 它們。

DynoTable 把一個項顯示為普通值,同時可檢視原始的 DynamoDB JSON。
DynoTable 把一個項顯示為普通值,同時可檢視原始的 DynamoDB JSON。

陷阱與後續步驟

  • DynamoDB JSON 裡數字是字串——{"N": "3"}。引號很要緊;別發出一個光禿禿的數字。
  • 集合與列表之分是一個建模決定,這套編碼把它顯式化了——慎重選擇(見資料型別)。
  • 在應用程式碼裡優先用 SDK 文件用戶端,而不是手工 marshal;把手寫 DynamoDB JSON 留給除錯和運算式。
  • 空字串對於非鍵屬性是允許的(自 2020 年起),但對錶鍵和索引鍵仍然被拒絕,而且歷來會絆倒工具鏈——把邊界情況驗證一下。

想把項當作普通值來瀏覽,而不是靠肉眼解碼型別標籤?下載 DynoTable,直接處理你的資料。

低階用戶端與文件用戶端

AWS SDK 提供兩層:

輸入形狀誰是元帥?
@aws-sdk/client-dynamodb(低階)DynamoDBJSONAttributeValue地圖您的程式碼或幫手
@aws-sdk/lib-dynamodb(文件)普通 JS 物件傳送/接收 SDK

應用程式程式碼應預設為 PutItem/GetItem 的文件用戶端。手動創作時獲取低階地圖 update expressions 或當圖書館期望時輸入的屬性值。

運算式屬性值也被編組

ConditionExpressionUpdateExpressionFilterExpression 預留位置 (:val, :inc) 對映到 ExpressionAttributeValues 中的編組值:

":status": {"S": "open"}
":count": {"N": "1"}

不匹配 — 在低階用戶端上傳送 "open" 而沒有 S 包裝器 — 返回ValidationException。的 expression builder 旁邊發出地圖運算式字串,以便預留位置和型別保持對齊。衝突的屬性名稱 reserved words使用 ExpressionAttributeNames (#st) 代替;檢查器工具輸出別名地圖準備貼上。

解析測試中的驚喜

編組中常見的測試失敗:

  • 空集 — DynamoDB 拒絕空 SS/NS/BS;省略屬性相反。
  • N 中浮動 — 線上路上將 "3.14" 作為字串傳送,而不是 JSON 數字。
  • Node 中的二進位制 — 文件用戶端中的 Uint8Array;原始 JSON 中的 base64。
  • 未定義的屬性 — 文件用戶端條undefined;低階用戶端可能會傳送無效的有效負載。當 Lambda 記錄原始 API 響應時,將一項貼上到 DynamoDB JSON converter 到可讀的普通 JSON 在與固定裝置進行比較之前。

標記的大小影響

每個型別包裝器都會新增位元組。一個扁平的JSON物件逐個欄位編組增長大約 30-40% 取決於屬性名稱——通貨膨脹所提供的 item size 和 RCU/WCU 舍入。大地圖使用短屬性名稱攤銷開銷;微小的布林標誌仍然值得他們的鍵名加上{"BOOL":true}。在批次載入編組項目之前,請檢查 item-size calculator 所以批次寫入不會意外超過 16 MB 請求限制。

DynoTable的兩種看法

項目編輯器每天都會保持不可見的編組——您編輯簡單的值,並在傳送時提交元帥。除錯複製自的生產項目時 CloudWatch 日誌,切換到 DynamoDB JSON 檢視以檢視確切的標籤,然後切換回來到 Plain JSON 進行編輯。匯出操作複製票證的任一表示形式和測試用例。

已更新