首页
/ Protobuf Editions 设计解析:JSON 处理行为如何从 proto2/proto3 分歧统一为 json_format 特性

Protobuf Editions 设计解析:JSON 处理行为如何从 proto2/proto3 分歧统一为 json_format 特性

2026-09-05 20:00:52作者:伍霜盼Ellen

本文为 Protobuf 设计文档 Edition Zero: JSON Handling 的技术解读。它回答了 Editions(Protobuf 新一代语法形态)必须解决的一个遗留问题:proto2 与 proto3 对 JSON 字段名冲突的校验策略互不兼容,如何用一个面向未来的 json_format 特性把这两种行为收敛到同一套 feature 体系里。读完后,你将理解当前运行时对 JSON 映射冲突的实际处理机制、deprecated_legacy_json_field_conflicts 选项的确切含义,以及 Editions 中 ALLOW / DISALLOW / LEGACY_BEST_EFFORT 三种状态的设计权衡与迁移路径。

背景:proto2 与 proto3 的 JSON 校验行为为何不兼容

Protobuf Editions 落地之前,Google 内部曾希望通过 JSON Field Name Conflicts(该内部文档未对外公开)这一项目统一两种语法对 JSON 映射的处理,但被部分内部使用场景阻塞,最终未能完成。

当前两种语法的默认行为存在根本分歧:

  • proto3 在解析阶段对 JSON 映射进行完整校验,要求映射唯一(uniqueness)。也就是说,同一消息内不允许两个 proto 字段映射到同一个 JSON 字段名,冲突会导致 protoc 报错。
  • proto2 采取尽力而为(best-effort)策略,允许 JSON 映射无法一一对应的情况存在。

这个分歧之所以阻塞 Editions 发布,是因为它无法用 Edition Zero features 中已定义的 feature 集合来表达。因此设计文档将其单独立项处理。

现状机制:字段名如何映射到 JSON,冲突在哪里产生

默认的 CamelCase 转换与 json_name 覆盖

按现状,Protobuf 会把每个字段名转换成一个 CamelCase 形式作为其 JSON 名称——这个转换结果永远合法,但不保证唯一。同时支持 json_name 字段选项来覆盖默认映射,用于 JSON 解析/序列化。正是这种覆盖机制让冲突成为可能:多个 proto 字段可以映射到同一个 JSON 字段。

设计文档归纳了当前 JSON 处理的四条行为基线:

  1. 所有 proto 消息都可以序列化为 JSON
    • 冲突的映射会产出带有重复键(duplicate keys)的 JSON;
  2. 所有 proto 消息都可以从 JSON 解析
    • 冲突映射导致行为未定义(undefined behavior)。在已遇到的所有案例中行为是确定性的,但在不同运行时之间不一致且出人意料
  3. protoc 默认会对 proto3 文件做校验:一旦检测到 JSON 冲突,解析即失败
    • 可通过 deprecated_legacy_json_field_conflicts 选项关闭该检查;
  4. proto2 文件仅在冲突双方都显式设置了 json_name 时才会解析失败
    • 若未设置 deprecated_legacy_json_field_conflicts,对默认 JSON 映射的冲突仍会发出警告(warn)。

本文的目标(也是原文档的 Recommendation)即:把上述行为统一为一个面向未来的 feature,作为 edition zero 的一部分。

源码佐证:唯一性检查在 protoc 中如何实现

在 C++ 编译器源码中,冲突检查的核心逻辑位于 src/google/protobuf/descriptor.cc

  • internal::DescriptorBuilder::CheckFieldJsonNameUniqueness同时执行两轮检查:一轮忽略 json_name(按默认 CamelCase 名比较),一轮采用 json_name。源码注释说明这是为了 field masks 等不使用 json_name 的场景服务;
  • 辅助结构 JsonNameDetails 通过 GetJsonNameDetails 计算每个字段的有效 JSON 名称:只有当字段显式设置了 json_name 且与 ToJsonName(name) 默认值不同时,才视为自定义名称。

这一实现正好对应设计文档描述的 proto3 语义——“新行为会把 json_name 纳入比较,并同样适用于 proto2”。

另外两个可验证的约束也出自同一实现(src/google/protobuf/descriptor.cc):

  • json_name 选项不允许出现在 extension 字段上
  • json_name 不允许包含内嵌的 null 字符。

deprecated_legacy_json_field_conflicts 选项本身定义在 src/google/protobuf/descriptor.protoFileOptions(字段号 11)与 EnumOptions(字段号 6中,均标注[deprecated = true]`。其官方注释解释了两种行为差异:

// Enable the legacy handling of JSON field name conflicts.  This lowercases
// and strips underscored from the fields before comparison in proto3 only.
// The new behavior takes `json_name` into account and applies to proto2 as
// well.
//
// This should only be used as a temporary measure against broken builds due
// to the change in behavior for JSON field name conflicts.

也就是说,legacy 模式下 proto3 的比较策略是“小写化并去掉下划线后再比较”,而新行为会把 json_name 计入比较并同样作用于 proto2。注释明确声明该选项只是临时手段(temporary measure),等待下游团队完成迁移后计划移除——这与 Editions 设计中把它映射为 LEGACY_BEST_EFFORT 的过渡性定位一脉相承。该选项的解析与序列化路径也有测试覆盖,参见 src/google/protobuf/compiler/parser_unittest.cc 中的相关用例。

设计建议:新增三态 json_format 特性

文档的推荐方案是:在 Edition Zero features 中新增 json_format 特性,取值为三态:

ALLOW

  • 字段在 proto 解析期间被完整校验;任何冲突的 JSON 映射都会触发 protoc 错误,从而保证映射唯一;
  • 与当前 proto3 行为一致;
  • 不需要运行时改动,因为该状态下 JSON 解析/序列化本身是允许的。

DISALLOW

  • 全面禁用 JSON 编码,并同时关闭所有与 JSON 映射相关的校验;
  • 当顶层消息设置该特性时,所有运行时在把消息解析/序列化为 JSON 时都必须失败;
  • 这是一个新增模式,提供不依赖任何 schema 改动的、LEGACY_BEST_EFFORT 之外的替代方案。

LEGACY_BEST_EFFORT

  • 字段会被校验正确性(correctness),但不校验唯一性(uniqueness);
  • 冲突的 JSON 映射只会触发 protoc 警告,不会报错;
  • 与当前 proto2 行为、或设置了 deprecated_legacy_json_field_conflicts 的 proto3 行为一致;
  • 由于这属于希望淘汰的未定义行为,会有并行努力在后续将其移除;
  • 不需要运行时改动,JSON 解析/序列化同样被允许。

目标与作用域

  • 该特性作用于 message 和 enum,同时为方便起见也提供文件级设置;
  • 长期来看,JSON 支持应该在 proto 层面显式声明。从 proto2/proto3 迁移时,按 syntaxdeprecated_legacy_json_field_conflicts 的取值,把一切分别映射为 ALLOWLEGACY_BEST_EFFORT

关键约束:ALLOW 消息禁止包含 DISALLOW 类型

文档还规定:任何 ALLOW 消息的整棵类型树(包括 extension,会直接编译失败)中不得出现 DISALLOW 类型。试图这样做会产生编译器错误。原文给出了三条理由:

  1. 实现更简单——大部分工作在 protoc 完成,运行时解析器只需检查顶层消息;
  2. 运行时失败不依赖消息内容——是否报错与本次实际序列化的数据无关;
  3. 避免所有权模糊——若某个 DISALLOW 字段偶尔被设置导致 bug,责任归属会含糊不清:是子类型的所有者应改为 ALLOW,还是引入依赖的父类型所有者负责?

需要注意一个例外:LEGACY_BEST_EFFORT 仍然允许对设置了 DISALLOW 的类型做序列化/解析。

DISALLOW 的现实用例

文档列举了两类使用场景:

  • 对应公开问题 protocolbuffers/protobuf#12525(无法使用 JSON 的场景);
  • 一些项目在运行时生成 proto 描述符,并用下划线来消除字段名的二义性。它们从不使用 JSON 格式,但当前却不得不绕开冲突检查。

与 features.json_format 双态定义的关系

在配套文档 Edition Zero Features 中,features.json_format 目前被定义为双态

  • ALLOW——运行时必须允许 JSON 解析与序列化,并在 proto 层面检查 JSON 映射是良定义的;
  • LEGACY_BEST_EFFORT——运行时尽力解析/序列化,允许产生运行时未定义行为的 proto(如多对一、一对多映射);
  • 默认值为 ALLOW(对应现有 proto3 行为),LEGACY_BEST_EFFORT 用于需要它的 proto2 文件(例如设置了 deprecated_legacy_json_field_conflicts 的文件)。

该文同时把 DISALLOW 列为长期演进方向:最终要么彻底移除该特性,要么用 DISALLOW 替代 LEGACY_BEST_EFFORT,在 proto 语言层面强制“标记为 ALLOW 的消息不得包含任何标记为 DISALLOW 的 message/enum(包括通过 extension 或字段引入)”。两篇文档对照可以看到:本 JSON Handling 设计稿正是上述 Future Work 的完整展开——它把双态升级为三态,并把“ALLOW 不得包含 DISALLOW”从构想细化为编译期约束与实现理由。

被否决的备选方案

设计文档完整保留了三个 Alternatives 及其利弊分析,它们展示了方案收敛的过程。

方案一:Dual State(仅 ALLOW/DISALLOW 二态)

不做三态,只做一个简单的 allow/disallow 特性。

  • 优点:概念上更简单;
  • 缺点:会被大量无法迁移的 proto 阻塞——这些 proto 正是 JSON Field Name Conflicts 项目中未能迁移的部分。其中一部分可以迁到 DISALLOW,但另一部分实际上依赖冲突下的现有运行时行为(作为 JSON 定制能力受限的一种 hack)。

方案二:默认 DISALLOW

把默认值从 ALLOW 改为 DISALLOW

  • 优点:内部大量 proto 只用于二进制/文本编码、根本不关心 JSON。默认 DISALLOW 可以:
    • 减少那些忘记显式设置 DISALLOW、且存在冲突 JSON 映射的团队的噪音;
    • 缩小支持面(support surface);
  • 缺点:需要确定 DISALLOW 应当添加到哪些位置(作用域界定问题)。

方案三:Do Nothing(不做)

  • 优点:短期省事,edition zero 保持简单;
  • 缺点
    • 迟早会撞上 JSON Field Name Conflicts 中的同样问题;
    • proto2/proto3 的现行行为互斥——edition zero 中不存在一个既能承接 proto2 又不冒破坏 proto3 风险的迁移目标。

小结

这篇设计文档的技术要点可以浓缩为三句话:

  1. 问题定位:proto3 的“JSON 映射必须唯一”与 proto2 的“尽力而为”无法共存于同一套 Editions feature 语义,必须在 proto 语言层显式表达该差异;
  2. 方案:新增三态 json_format 特性(ALLOW / DISALLOW / LEGACY_BEST_EFFORT),默认 ALLOW 保持 proto3 行为,LEGACY_BEST_EFFORT 承接 proto2 及设置了 deprecated_legacy_json_field_conflicts 的文件,DISALLOW 作为全新的“禁用 JSON”模式,并辅以“ALLOW 类型树中禁止出现 DISALLOW 类型”的编译期约束;
  3. 落地证据:现状侧的冲突检查(含 json_name 双轮校验)、扩展字段禁用 json_name、以及 src/google/protobuf/descriptor.proto 中已标记废弃的 deprecated_legacy_json_field_conflicts 选项,都从源码层面印证了设计文档所描述的行为边界与迁移过渡状态。

延伸阅读:What are Protobuf Editions?Edition Zero Features,以及 Editions 语义的测试默认值定义 editions/defaults.bzl

登录后查看全文
热门项目推荐
相关项目推荐