Protobuf 转 JSON 默认字段不显示:presence 语义与打印选项
C++ 服务把 protobuf 消息转成 JSON 交给下游时,会遇到一类问题:取默认值的字段——bool 的 false、数值的 0、空字符串——在 JSON 输出里直接消失。下游拿到 {"a": "1234"} 这样的对象,无法区分「flag 字段没设置」和「flag 是 false」,而这两种情况的业务含义往往不同。本文说明这一行为的来源,以及让字段强制输出的方法,适用于 Proto3 语法与 C++ 实现。
现象
消息定义:
message TestData
{
bool flag = 1;
int64 a = 2;
uint64 b = 3;
double c = 4;
string s = 5;
} 给 a、b 赋非零值,其余字段保持默认,序列化结果:
{
"a": "1234",
"b": "999"
} flag、c、s 三个字段整体消失——不是值为默认,而是键都不存在。
原因:Proto3 JSON 映射省略默认值字段
这不是 bug。protobuf 官方的 JSON 映射规范规定:不支持字段存在性(presence)的字段,取默认值时序列化器应当省略。Proto3 里不带 optional 的标量字段正属于这一类——这类字段不跟踪「是否被设置过」,取默认值与从未赋值无法区分,序列化器统一按省略处理。
两条伴生规则值得一起记住:
- 64 位整数(
int64/uint64/fixed64)在 JSON 中映射为字符串。上例的"a": "1234"带引号就是这个原因:部分 JSON 实现用双精度浮点解析数字,超出 2^53 会丢失精度,规范用字符串规避; - 有 presence 的字段(
optional标量、message 字段、repeated/map)不受「默认省略」约束,输出与否按 has 语义判断。
解法:打印选项
MessageToJsonString 支持通过 JsonPrintOptions 控制输出:
google::protobuf::util::JsonPrintOptions options;
options.always_print_primitive_fields = true; // 默认值字段也输出
std::string json;
google::protobuf::util::MessageToJsonString(msg, &json, options); 加上选项后,同一个消息的输出:
{
"flag": false,
"a": "1234",
"b": "999",
"c": 0,
"s": ""
} 版本注记:新版 protobuf 的统一 JSON API(google/protobuf/json/json.h 的 PrintOptions)中,这个选项改名为 always_print_fields_with_no_presence,旧路径 util/json_util.h 已经变成转发别名。用哪套头文件,就按哪套字段名写。
optional 字段不受此选项影响
如果字段本身带 optional:
optional bool is_debug = 3; 它就有了 presence,行为与隐式字段不同:
- 从未设置:即使开启 always-print 选项,字段也不会出现在 JSON 中。需要区分「无值」和「假值」时,这正是
optional的用途——业务侧用has_is_debug()判断,JSON 侧字段缺席即未设置; - 显式设置为 false(调用过
set_is_debug(false)):字段会输出false。
官方文档同时注明,截至 v25.x,C++ 等实现对 always-print 选项作用于 proto3 optional 字段的支持不完全,实际行为以所用版本为准。
使用建议
- JSON 转换涉及反射和内存分配,比二进制序列化(
SerializeToString)慢,适合放在系统边界(如 HTTP 响应)做,内部链路用二进制格式; JsonPrintOptions可以在调用点按值缓存复用,避免每次转换重新构造;- 如果下游需要「字段永远在」的契约,把打印选项固定进统一的转换函数,不要散落在各调用点。
小结
默认值字段消失的根因是 Proto3 隐式 presence 字段的省略规则;always_print_primitive_fields(新 API 中为 always_print_fields_with_no_presence)控制这类字段强制输出;optional 字段按 has 语义输出,缺席即未设置,与打印选项无关。