Protobuf Editions 设计解析:JSON 处理行为如何从 proto2/proto3 分歧统一为 json_format 特性
本文为 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 处理的四条行为基线:
- 所有 proto 消息都可以序列化为 JSON
- 冲突的映射会产出带有重复键(duplicate keys)的 JSON;
- 所有 proto 消息都可以从 JSON 解析
- 冲突映射导致行为未定义(undefined behavior)。在已遇到的所有案例中行为是确定性的,但在不同运行时之间不一致且出人意料;
- protoc 默认会对 proto3 文件做校验:一旦检测到 JSON 冲突,解析即失败
- 可通过
deprecated_legacy_json_field_conflicts选项关闭该检查;
- 可通过
- 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.proto 的 FileOptions(字段号 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 迁移时,按
syntax和deprecated_legacy_json_field_conflicts的取值,把一切分别映射为ALLOW或LEGACY_BEST_EFFORT。
关键约束:ALLOW 消息禁止包含 DISALLOW 类型
文档还规定:任何 ALLOW 消息的整棵类型树(包括 extension,会直接编译失败)中不得出现 DISALLOW 类型。试图这样做会产生编译器错误。原文给出了三条理由:
- 实现更简单——大部分工作在 protoc 完成,运行时解析器只需检查顶层消息;
- 运行时失败不依赖消息内容——是否报错与本次实际序列化的数据无关;
- 避免所有权模糊——若某个
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 风险的迁移目标。
小结
这篇设计文档的技术要点可以浓缩为三句话:
- 问题定位:proto3 的“JSON 映射必须唯一”与 proto2 的“尽力而为”无法共存于同一套 Editions feature 语义,必须在 proto 语言层显式表达该差异;
- 方案:新增三态
json_format特性(ALLOW/DISALLOW/LEGACY_BEST_EFFORT),默认ALLOW保持 proto3 行为,LEGACY_BEST_EFFORT承接 proto2 及设置了deprecated_legacy_json_field_conflicts的文件,DISALLOW作为全新的“禁用 JSON”模式,并辅以“ALLOW类型树中禁止出现DISALLOW类型”的编译期约束; - 落地证据:现状侧的冲突检查(含
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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00