ProtoJSON 格式

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

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.Structgoogle.protobuf.Value 类型来允许将任意 JSON 解析到 Protobuf schema 中,但这只允许您将这些值捕获为无 schema 的无序键值对。

效率不如二进制线路格式

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

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

没有像二进制线路格式那样好的 schema 演进保证

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

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

格式说明

每种类型的表示

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

ProtobufJSONJSON 示例说明
messageobject{"fooBar": v, "g": null, ...}生成 JSON 对象。消息字段名被映射为 lowerCamelCase(小驼峰命名法)并成为 JSON 对象键。如果指定了 json_name 字段选项,则将使用指定的值作为键。解析器接受 lowerCamelCase 名称(或由 json_name 选项指定的名称)和原始 proto 字段名。null 是所有字段类型都接受的值,它会使字段保持未设置状态。\0 (nul) 不能在 json_name 值中使用。有关原因的更多信息,请参见对 json_name 更严格的验证
enumstring"FOO_BAR"使用 proto 中指定的枚举值名称。解析器接受枚举名称和整数值。
map<K,V>object{"k": v, ...}所有键都将转换为字符串(JSON 规范中键只能是字符串)。
repeated Varray[v, ...]null 被接受为空列表 []
booltrue, falsetrue, false
stringstring"Hello World!"
bytesbase64 字符串"YWJjMTIzIT8kKiYoKSctPUB+"JSON 值将是使用标准 base64 编码(带填充)编码为字符串的数据。接受标准或 URL 安全的 base64 编码,无论有无填充。
int32, fixed32, uint32number1, -10, 0JSON 值将是一个十进制数。接受数字或字符串。空字符串无效。接受指数表示法(如 `1e2`),无论是否带引号。
int64, fixed64, uint64string"1", "-10"JSON 值将是一个十进制字符串。接受数字或字符串。空字符串无效。接受指数表示法(如 `1e2`),无论是否带引号。
float, doublenumber1.1, -10.0, 0, "NaN", "Infinity"JSON 值将是一个数字或特殊字符串值 "NaN"、"Infinity" 和 "-Infinity" 之一。接受数字或字符串。空字符串无效。也接受指数表示法。
Anyobject{"@type": "url", "f": v, ... }如果 Any 包含一个在此表中有特殊 JSON 映射的知名类型(例如 google.protobuf.Duration),它将被转换如下:{"@type": xxx, "value": yyy}。否则,该值将像往常一样转换为 JSON 对象,并插入一个额外的 "@type" 字段,其值为指示消息类型的 URL。
Timestampstring"1972-01-01T10:00:20.021Z"使用 RFC 3339(参见澄清),生成的输出将始终进行 Z 标准化,并使用 0、3、6 或 9 位小数。也接受除 "Z" 之外的偏移量。
Durationstring"1.000340012s", "1s"生成的输出总是包含 0、3、6 或 9 位小数(取决于所需精度),后跟后缀 "s"。只要小数位数不超过纳秒精度,接受任意位数(也可以没有),并且需要后缀 "s"。
Structobject{ ... }任何 JSON 对象。参见 struct.proto
包装器类型各种类型2, "2", "foo", true, "true", null, 0, ...包装器在 JSON 中的表示与其包装的基本类型相同,但允许使用 null 并且在数据转换和传输过程中会保留它。
FieldMaskstring"f.fooBar,h"参见 field_mask.proto
ListValuearray[foo, bar, ...]
Valuevalue任何 JSON 值。详情请查阅 google.protobuf.Value
NullValuenullJSON null。这是[null 解析行为](#null-values)的特例。
Emptyobject{}一个空的 JSON 对象

存在性和默认值

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

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

Null 值

序列化器不应输出 null 值。

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

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

null 值不允许出现在 repeated 字段中。

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

重复值

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

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

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

超出范围的数值

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

ProtoJSON 线路安全

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

JSON 线路不安全的变更

线路不安全的变更是指,如果您使用新 schema 的解析器来解析使用旧 schema 序列化的数据(反之亦然),将会破坏系统的 schema 变更。您几乎永远不应该进行这种形式的 schema 变更。

  • 将字段更改为或从具有相同编号和类型的扩展更改是不安全的。
  • 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 读取的客户端将丢弃高位比特。

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

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

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

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

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

兼容但可能存在数据丢失

  • 在任何 32 位整数(int32uint32sint32sfixed32fixed32)和任何 64 位整数(int64uint64sint64sfixed64)之间进行更改是兼容的变更。
    • 如果从线路解析出的数字不适用于相应类型,您将得到与在 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 字段,但不影响 proto3 optional 字段。计划在未来版本中修复此问题。

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

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

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