ProtoJSON 格式
Protobuf 支持一种规范的 JSON 编码,这使得与不支持标准 Protobuf 二进制线路格式的系统共享数据变得更加容易。
本页指定了该格式,但定义一个合规的 ProtoJSON 解析器所需的许多额外边缘情况已在 Protobuf 一致性测试套件中涵盖,此处不作详尽阐述。
格式的非目标
无法表示某些 JSON schema
ProtoJSON 格式旨在成为可用 Protobuf schema 语言表达的 schema 的 JSON 表示形式。
尽管可能将许多已有的 JSON schema 表示为 Protobuf schema 并使用 ProtoJSON 进行解析,但它并非旨在能够表示任意的 JSON schema。
例如,无法在 Protobuf schema 中表达像 number[][]
或 number|string
这样在 JSON schema 中常见的类型。
可以使用 google.protobuf.Struct
和 google.protobuf.Value
类型来允许将任意 JSON 解析到 Protobuf schema 中,但这只允许您将这些值捕获为无 schema 的无序键值对。
效率不如二进制线路格式
ProtoJSON 格式的效率不如二进制线路格式,并且永远不会。
转换器在编码和解码消息时使用更多的 CPU,并且(除少数情况外)编码后的消息占用更多空间。
没有像二进制线路格式那样好的 schema 演进保证
ProtoJSON 格式不支持未知字段,并且它将字段和枚举值的名称放入编码后的消息中,这使得以后更改这些名称变得更加困难。删除字段是一种破坏性变更,会触发解析错误。
有关更多详细信息,请参见下面的 JSON 线路安全。
格式说明
每种类型的表示
下表显示了数据在 JSON 文件中的表示方式。
Protobuf | JSON | JSON 示例 | 说明 |
---|---|---|---|
message | object | {"fooBar": v, "g": null, ...} | 生成 JSON 对象。消息字段名被映射为 lowerCamelCase(小驼峰命名法)并成为 JSON 对象键。如果指定了 json_name 字段选项,则将使用指定的值作为键。解析器接受 lowerCamelCase 名称(或由 json_name 选项指定的名称)和原始 proto 字段名。null 是所有字段类型都接受的值,它会使字段保持未设置状态。\0 (nul) 不能在 json_name 值中使用。有关原因的更多信息,请参见对 json_name 更严格的验证。 |
enum | string | "FOO_BAR" | 使用 proto 中指定的枚举值名称。解析器接受枚举名称和整数值。 |
map<K,V> | object | {"k": v, ...} | 所有键都将转换为字符串(JSON 规范中键只能是字符串)。 |
repeated V | array | [v, ...] | null 被接受为空列表 [] 。 |
bool | true, false | true, false | |
string | string | "Hello World!" | |
bytes | base64 字符串 | "YWJjMTIzIT8kKiYoKSctPUB+" | JSON 值将是使用标准 base64 编码(带填充)编码为字符串的数据。接受标准或 URL 安全的 base64 编码,无论有无填充。 |
int32, fixed32, uint32 | number | 1, -10, 0 | JSON 值将是一个十进制数。接受数字或字符串。空字符串无效。接受指数表示法(如 `1e2`),无论是否带引号。 |
int64, fixed64, uint64 | string | "1", "-10" | JSON 值将是一个十进制字符串。接受数字或字符串。空字符串无效。接受指数表示法(如 `1e2`),无论是否带引号。 |
float, double | number | 1.1, -10.0, 0, "NaN", "Infinity" | JSON 值将是一个数字或特殊字符串值 "NaN"、"Infinity" 和 "-Infinity" 之一。接受数字或字符串。空字符串无效。也接受指数表示法。 |
Any | object | {"@type": "url", "f": v, ... } | 如果 Any 包含一个在此表中有特殊 JSON 映射的知名类型(例如 google.protobuf.Duration ),它将被转换如下:{"@type": xxx, "value": yyy} 。否则,该值将像往常一样转换为 JSON 对象,并插入一个额外的 "@type" 字段,其值为指示消息类型的 URL。 |
Timestamp | string | "1972-01-01T10:00:20.021Z" | 使用 RFC 3339(参见澄清),生成的输出将始终进行 Z 标准化,并使用 0、3、6 或 9 位小数。也接受除 "Z" 之外的偏移量。 |
Duration | string | "1.000340012s", "1s" | 生成的输出总是包含 0、3、6 或 9 位小数(取决于所需精度),后跟后缀 "s"。只要小数位数不超过纳秒精度,接受任意位数(也可以没有),并且需要后缀 "s"。 |
Struct | object | { ... } | 任何 JSON 对象。参见 struct.proto 。 |
包装器类型 | 各种类型 | 2, "2", "foo", true, "true", null, 0, ... | 包装器在 JSON 中的表示与其包装的基本类型相同,但允许使用 null 并且在数据转换和传输过程中会保留它。 |
FieldMask | string | "f.fooBar,h" | 参见 field_mask.proto 。 |
ListValue | array | [foo, bar, ...] | |
Value | value | 任何 JSON 值。详情请查阅 google.protobuf.Value。 | |
NullValue | null | JSON null。这是[null 解析行为](#null-values)的特例。 | |
Empty | object | {} | 一个空的 JSON 对象 |
存在性和默认值
当从 protocol buffer 生成 JSON 编码的输出时,如果一个字段支持存在性(presence),序列化器必须且仅当相应的 hasser 返回 true 时才输出该字段的值。
如果字段不支持字段存在性且其值为默认值(例如,任何空的 repeated 字段),序列化器应在输出中省略它。实现可以提供选项以在输出中包含具有默认值的字段。
Null 值
序列化器不应输出 null
值。
解析器应接受 null
作为任何字段的合法值,其行为如下:
- 任何键的有效性检查仍应进行(不允许未知字段)。
- 字段应保持未设置状态,就好像它根本不存在于输入中一样(在适用的情况下,hasser 仍应返回 false)。
null
值不允许出现在 repeated 字段中。
google.protobuf.NullValue
是此行为的一个特例:对于此类型,null
被视为一个哨兵存在值(sentinel-present value),因此序列化器和解析器必须根据标准存在性行为处理此类型的字段。这种行为相应地允许 google.protobuf.Struct
和 google.protobuf.Value
无损地往返任意 JSON。
重复值
序列化器绝不能在同一个 JSON 对象中多次序列化同一个字段,也不能在同一个 oneof 中序列化多个不同的 case。
解析器应接受同一字段的重复出现,并保留最后提供的值。这也适用于同一字段名的“备用拼写”。
如果实现无法维护有关字段顺序的必要信息,则首选拒绝带有重复键的输入,而不是让任意值胜出。在某些实现中,维护对象的字段顺序可能不切实际或不可行,因此强烈建议系统在可能的情况下避免依赖 ProtoJSON 中重复字段的特定行为。
超出范围的数值
解析数值时,如果从线路解析出的数字不适用于相应类型,解析器应将该值强制转换为适当的类型。这与 C++ 或 Java 中的简单类型转换具有相同的行为(例如,如果一个大于 2^32 的数字被读取到一个 int32 字段,它将被截断为 32 位)。
ProtoJSON 线路安全
使用 ProtoJSON 时,只有某些 schema 变更在分布式系统中是安全的。这与应用于二进制线路格式的相同概念形成对比。
JSON 线路不安全的变更
线路不安全的变更是指,如果您使用新 schema 的解析器来解析使用旧 schema 序列化的数据(反之亦然),将会破坏系统的 schema 变更。您几乎永远不应该进行这种形式的 schema 变更。
- 将字段更改为或从具有相同编号和类型的扩展更改是不安全的。
- 在
string
和bytes
之间更改字段是不安全的。 - 在消息类型和
bytes
之间更改字段是不安全的。 - 将任何字段从
optional
更改为repeated
是不安全的。 - 在
map<K, V>
和相应的repeated
消息字段之间更改字段是不安全的。 - 将字段移入一个现有的
oneof
是不安全的。
JSON 线路安全的变更
线路安全的变更是指完全安全地演进模式,而不会有数据丢失或新的解析失败的风险。
请注意,几乎所有线路安全的变更都可能对应用程序代码造成破坏性变更。例如,向已有的枚举添加一个值,对于任何对该枚举使用详尽 switch 语句的代码来说,都将是编译中断。因此,Google 可能会避免对公共消息进行某些此类变更。AIPs 中包含关于哪些变更在那里是安全的指导。
- 将单个
optional
字段更改为新的oneof
的成员是安全的。 - 将仅包含一个字段的
oneof
更改为optional
字段是安全的。 - 在
int32
、sint32
、sfixed32
、fixed32
之间更改字段是安全的。 - 在
int64
、sint64
、sfixed64
、fixed64
之间更改字段是安全的。 - 更改字段编号是安全的(因为字段编号在 ProtoJSON 格式中不使用),但仍然强烈不鼓励,因为它在二进制线路格式中非常不安全。
- 如果所有相关客户端都设置了“将枚举值作为整数发出”(参见选项),则向枚举添加值是安全的。
JSON 线路兼容的变更(有条件安全)
与线路安全的变更不同,线路兼容意味着相同的数据在给定变更前后都可以被解析。但是,读取它的客户端在这种变更下会得到有损的数据。例如,将 int32 更改为 int64 是一个兼容的变更,但如果写入一个大于 INT32_MAX 的值,一个将其作为 int32 读取的客户端将丢弃高位比特。
只有在您仔细管理系统推出过程的情况下,才能对您的 schema 进行兼容的变更。例如,您可以将 int32 更改为 int64,但要确保在将新 schema 部署到所有端点之前,继续只写入合法的 int32 值,之后再开始写入更大的值。
兼容但有未知字段处理问题
与二进制线路格式不同,ProtoJSON 实现通常不传播未知字段。这意味着向 schema 添加内容通常是兼容的,但如果使用旧 schema 的客户端观察到新内容,则会导致解析失败。
这意味着您可以向您的 schema 添加内容,但在您知道 schema 已部署到相关客户端或服务器(或相关客户端设置了忽略未知字段标志,见下文讨论)之前,不能安全地开始写入它们。
- 在此警告下,添加和删除字段被认为是兼容的。
- 在此警告下,删除枚举值被认为是兼容的。
兼容但可能存在数据丢失
- 在任何 32 位整数(
int32
、uint32
、sint32
、sfixed32
、fixed32
)和任何 64 位整数(int64
、uint64
、sint64
、sfixed64
)之间进行更改是兼容的变更。- 如果从线路解析出的数字不适用于相应类型,您将得到与在 C++ 中将该数字强制转换为该类型相同的效果(例如,如果一个 64 位数字被读取为 int32,它将被截断为 32 位)。
- 与二进制线路格式不同,
bool
与整数不兼容。 - 请注意,int64 类型默认带引号,以避免在作为 double 或 JavaScript number 处理时精度丢失,而 32 位类型默认不带引号。合规的实现将接受所有整数类型的这两种情况,但不合规的实现可能会错误处理这种情况,不处理带引号的 int32 或不带引号的 int64,这可能会在此类变更下中断。
enum
可能有条件地与string
兼容- 如果任何客户端使用了“enums-as-ints”标志,则枚举将与整数类型兼容。
RFC 3339 澄清
RFC 3339 意在声明 ISO-8601 格式的一个严格子集,不幸的是,由于 RFC 3339 于 2002 年发布,而 ISO-8601 随后进行了修订,却没有对 RFC 3339 进行相应修订,从而产生了一些歧义。
最值得注意的是,ISO-8601-1988 包含以下注释:
在日期和时间表示中,当大写字符不可用时,可以使用小写字符。
这个注释是否建议解析器通常应接受小写字母,还是仅建议在技术上无法使用大写字母的环境中,小写字母可以用作替代品,这一点是模棱两可的。RFC 3339 包含一个注释,意在澄清解释为通常应接受小写字母。
ISO-8601-2019 不包含相应的注释,并且明确规定不允许使用小写字母。这给所有声称支持 RFC 3339 的库带来了一些困惑:今天,RFC 3339 声明它是 ISO-8601 的一个配置文件,但包含一个引用了最新 ISO-8601 规范中已不存在的内容的注释。
ProtoJSON 规范决定时间戳格式是“RFC 3339 作为 ISO-8601-2019 的配置文件”的更严格定义。一些 Protobuf 实现可能因为使用了实现为“RFC 3339 作为 ISO-8601-1988 的配置文件”的时间戳解析实现而不合规,这将接受一些额外的边缘情况。
为实现一致的互操作性,解析器应尽可能只接受更严格的子集格式。当使用接受较宽松定义的非合规实现时,强烈避免依赖于接受这些额外的边缘情况。
JSON 选项
一个合规的 protobuf JSON 实现可以提供以下选项:
始终输出无存在性(presence)的字段:默认情况下,不支持存在性且具有默认值的字段在 JSON 输出中被省略(例如,值为 0 的隐式存在性整数、空字符串的隐式存在性字符串字段,以及空的 repeated 和 map 字段)。实现可以提供一个选项来覆盖此行为并输出具有默认值的字段。
截至 v25.x,C++、Java 和 Python 实现不合规,因为此标志影响 proto2
optional
字段,但不影响 proto3optional
字段。计划在未来版本中修复此问题。忽略未知字段:protobuf JSON 解析器默认应拒绝未知字段,但可以提供一个选项以在解析时忽略未知字段。
使用 proto 字段名而非 lowerCamelCase 名称:默认情况下,protobuf JSON 打印器应将字段名转换为 lowerCamelCase 并将其用作 JSON 名称。实现可以提供一个选项,以使用 proto 字段名作为 JSON 名称。Protobuf JSON 解析器必须接受转换后的 lowerCamelCase 名称和 proto 字段名。
将枚举值作为整数而非字符串发出:默认情况下,在 JSON 输出中使用枚举值的名称。可以提供一个选项,以使用枚举值的数值代替。