入门阅读约 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 进行编辑。导出操作复制票证的任一表示形式和测试用例。

更新于