ProtoJSON 格式

介绍如何使用 Protobuf 到 JSON 的转换工具。

Protobuf 支持一种规范的 JSON 编码,这使得与不支持标准 Protobuf 二进制线路格式的系统共享数据变得更加容易。

此页面指定了格式,但定义一个合规的 ProtoJSON 解析器的许多其他边界情况已在 Protobuf 一致性测试套件中涵盖,此处不作详尽说明。

格式的非目标

无法表示某些 JSON 模式

ProtoJSON 格式旨在成为可在 Protobuf 模式语言中表达的模式的 JSON 表示。

虽然可能将许多预先存在的 JSON 模式表示为 Protobuf 模式并使用 ProtoJSON 解析,但它并非旨在能够表示任意 JSON 模式。

例如,无法在 Protobuf 模式中表达像 number[][]number|string 这样在 JSON 模式中常见的类型。

可以使用 google.protobuf.Structgoogle.protobuf.Value 类型来允许将任意 JSON 解析为 Protobuf 模式,但这只能将值捕获为无模式的无序键值映射。

效率不如二进制线路格式高

ProtoJSON 格式的效率不如二进制线路格式,并且永远不会。

转换器在编码和解码消息时使用更多的 CPU,并且(除了极少数情况)编码后的消息占用更多空间。

模式演进保证不如二进制线路格式好

ProtoJSON 格式不支持未知字段,并且它将字段和枚举值的名称放入编码消息中,这使得以后更改这些名称变得更加困难。删除字段是一种破坏性变更,会触发解析错误。

有关更多详细信息,请参阅下面的JSON 线路安全

格式说明

每种类型的表示

下表显示了数据在 JSON 文件中的表示方式。

ProtobufJSONJSON 示例说明
消息 (message)对象 (object){"fooBar": v, "g": null, ...}生成 JSON 对象。消息字段名称映射为小驼峰命名法 (lowerCamelCase),并成为 JSON 对象的键。如果指定了 json_name 字段选项,则将使用指定的值作为键。解析器接受小驼峰命名法名称(或由 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, falsetrue, false
字符串 (string)字符串 (string)"Hello World!"
字节 (bytes)base64 字符串"YWJjMTIzIT8kKiYoKSctPUB+"JSON 值将是使用带填充的标准 base64 编码为字符串的数据。接受标准或 URL 安全的带/不带填充的 base64 编码。
int32, fixed32, uint32数字 (number)1, -10, 0JSON 值将是一个十进制数。接受数字或字符串。空字符串无效。指数表示法(如 `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 映射的值,它将按如下方式转换:{"@type": xxx, "value": yyy}。否则,该值将被转换为 JSON 对象,并插入 "@type" 字段以指示实际数据类型。
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
NullValuenullJSON 空值。是[空值解析行为](#null-values)的特例。
Empty对象 (object){}一个空的 JSON 对象

存在性与默认值

从协议缓冲区生成 JSON 编码的输出时,如果一个字段支持存在性(presence),序列化器当且仅当相应的 hasser 返回 true 时才必须发出该字段的值。

如果字段不支持字段存在性且具有默认值(例如任何空的重复字段),序列化器应在输出中省略它。实现可以提供选项以在输出中包含具有默认值的字段。

空值

序列化器不应发出 null 值。

解析器应接受 null 作为任何字段的合法值,其行为如下

  • 任何键的有效性检查仍应进行(不允许未知字段)。
  • 该字段应保持未设置状态,就好像它根本不存在于输入中一样(适用的情况下,hasser 仍应返回 false)。

不允许在重复字段内使用 null 值。

google.protobuf.NullValue 是此行为的一个特例:对于此类型,null 被视为一个哨兵存在值,因此该类型的字段必须由序列化器和解析器根据标准存在性行为处理。此行为相应地允许 google.protobuf.Structgoogle.protobuf.Value 无损地往返任意 JSON。

重复值

序列化器决不能在同一个 JSON 对象中多次序列化同一个字段,也不能在同一个 oneof 中序列化多个不同的 case。

解析器应接受同一字段的重复,并保留最后提供的值。这也适用于同一字段名称的“备用拼写”。

如果实现无法维护有关字段顺序的必要信息,最好拒绝具有重复键的输入,而不是让任意值胜出。在某些实现中,维护对象的字段顺序可能不切实际或不可行,因此强烈建议系统在可能的情况下避免在 ProtoJSON 中依赖重复字段的特定行为。

超出范围的数值

解析数值时,如果从线路解析的数字不适合相应类型,解析器应将该值强制转换为适当的类型。这与 C++ 或 Java 中的简单类型转换具有相同的行为(例如,如果一个大于 2^32 的数字被读取为 int32 字段,它将被截断为 32 位)。

ProtoJSON 线路安全

使用 ProtoJSON 时,在分布式系统中只有某些模式变更是安全的。这与应用于二进制线路格式的相同概念形成对比。

JSON 线路不安全的变更

线路不安全的变更是指,如果您使用新模式的解析器来解析使用旧模式序列化的数据(反之亦然),将会导致中断的模式变更。您应该几乎从不进行这种形式的模式变更。

  • 将字段更改为或从具有相同编号和类型的扩展字段更改是不安全的。
  • stringbytes 之间更改字段是不安全的。
  • 在消息类型和 bytes 之间更改字段是不安全的。
  • 将任何字段从 optional 更改为 repeated 是不安全的。
  • map<K, V> 和相应的 repeated 消息字段之间更改字段是不安全的。
  • 将字段移入一个现有的 oneof 是不安全的。

JSON 线路安全的变更

线路安全的变更是指完全安全地演进模式,而不会有数据丢失或新的解析失败的风险。

请注意,几乎所有线路安全的变更都可能对应用程序代码造成破坏性变更。例如,向预先存在的枚举添加一个值,对于任何具有该枚举的详尽 switch 语句的代码来说,都会是编译中断。因此,Google 可能会避免在公共消息上进行某些此类变更。AIPs 中包含了关于哪些变更在那里可以安全进行的指导。

  • 将单个 optional 字段更改为新的 oneof 的成员是安全的。
  • 将仅包含一个字段的 oneof 更改为 optional 字段是安全的。
  • int32sint32sfixed32fixed32 之间更改字段是安全的。
  • int64sint64sfixed64fixed64 之间更改字段是安全的。
  • 更改字段编号是安全的(因为字段编号未在 ProtoJSON 格式中使用),但仍然强烈不建议这样做,因为它在二进制线路格式中非常不安全。
  • 如果所有相关客户端都设置了“将枚举值作为整数发出”(请参阅选项),则向枚举添加值是安全的。

JSON 线路兼容的变更(有条件安全)

与线路安全变更不同,线路兼容意味着相同的数据可以在给定变更前后进行解析。但是,读取它的客户端在这种形式的变更下会得到有损的数据。例如,将 int32 更改为 int64 是一个兼容的变更,但是如果写入了一个大于 INT32_MAX 的值,将其作为 int32 读取的客户端将丢弃高位比特。

您只有在仔细管理系统部署的情况下,才能对您的模式进行兼容的变更。例如,您可以将 int32 更改为 int64,但要确保在将新模式部署到所有端点之前继续只写入合法的 int32 值,然后在之后开始写入更大的值。

兼容但有未知字段处理问题

与二进制线路格式不同,ProtoJSON 实现通常不传播未知字段。这意味着向模式添加内容通常是兼容的,但如果使用旧模式的客户端观察到新内容,则会导致解析失败。

这意味着您可以向您的模式添加内容,但在您知道模式已部署到相关客户端或服务器(或相关客户端设置了“忽略未知字段”标志,如下文所述)之前,不能安全地开始写入它们。

  • 在有此警告的情况下,添加和删除字段被认为是兼容的。
  • 在有此警告的情况下,删除枚举值被认为是兼容的。

兼容但可能存在数据丢失

  • 在任何 32 位整数(int32uint32sint32sfixed32fixed32)和任何 64 位整数(int64uint64sint64sfixed32)之间进行更改是兼容的变更。
    • 如果从线路解析的数字不适合相应的类型,您将得到与在 C++ 中将数字强制转换为该类型相同的效果(例如,如果将 64 位数字作为 int32 读取,它将被截断为 32 位)。
    • 与二进制线路格式不同,bool 与整数不兼容。
    • 请注意,int64 类型默认带引号,以避免在作为 double 或 JavaScript 数字处理时损失精度,而 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 实现可以提供以下选项

  • 始终发出没有存在性的字段:默认情况下,不支持存在性且具有默认值的字段在 JSON 输出中被省略(例如,值为 0 的隐式存在性整数,空字符串的隐式存在性字符串字段,以及空的重复字段和映射字段)。实现可以提供一个选项来覆盖此行为并输出具有默认值的字段。

    截至 v25.x,C++、Java 和 Python 的实现是不合规的,因为此标志影响 proto2 的 optional 字段,但不影响 proto3 的 optional 字段。计划在未来版本中修复。

  • 忽略未知字段:protobuf JSON 解析器默认应拒绝未知字段,但可以提供一个选项以在解析时忽略未知字段。

  • 使用 proto 字段名称而不是小驼峰命名法名称:默认情况下,protobuf JSON 打印器应将字段名称转换为小驼峰命名法并将其用作 JSON 名称。实现可以提供一个选项来使用 proto 字段名称作为 JSON 名称。Protobuf JSON 解析器必须接受转换后的小驼峰命名法名称和 proto 字段名称。

  • 将枚举值作为整数而不是字符串发出:默认情况下,在 JSON 输出中使用枚举值的名称。可以提供一个选项来改用枚举值的数值。