第3章 请求与响应格式规范:JSON/ProtoBuf 选型、字段命名、枚举约定

请求与响应格式是模块边界 API 契约中最外显的协议层。选型错误或命名混乱会直接导致解析失败、跨语言互通成本上升、枚举扩展断裂。本章聚焦三类核心决策:序列化格式选型、字段命名体系、枚举值约定,并给出可直接落地的契约示例。

核心原则:格式规范的首要目标是“可预测”——消费者无需查阅文档就能推断字段含义、枚举范围与序列化行为。任何偏离惯例的例外都必须通过显式契约注释说明。

3.1 序列化格式选型:JSON 与 ProtoBuf 的取舍

选型不是技术偏好,而是对可读性、性能、版本兼容性、工具链成熟度的综合权衡。下表从七个关键维度对比两种格式:

维度 JSON ProtoBuf
序列化类型 文本(UTF‑8) 二进制
可读性 高,可直接阅读与调试 低,需配合 .proto 与工具反序列化
序列化/反序列化性能 中等(JSON 解析器开销) 高(紧凑二进制,零拷贝可能)
模式定义(Schema) 可选(JSON Schema)或隐式 强制(.proto IDL)
版本兼容性 依赖字段存在性检查与文档约定 原生支持字段编号、默认值、保留字段
字段编号 无(字段按名称匹配) 必须为每个字段分配唯一编号(1–229‑1)
典型场景 Web API、浏览器客户端、快速原型 内部微服务、高吞吐 RPC、移动端
  1. 选 JSON 的场景:消费者包含浏览器/前端、需要人工调试、团队 ProtoBuf 经验不足、API 暴露给外部生态。
  2. 选 ProtoBuf 的场景:模块间高频调用、带宽敏感、强 Schema 管控、多语言自动代码生成。
  3. 混合策略:内部 RPC 使用 ProtoBuf,Edge Gateway 对外暴露 RESTful JSON,通过适配层转换。
选型陷阱:不要仅因“团队熟悉”而忽略 ProtoBuf 的版本兼容优势。在超过 10 个消费者、每年迭代 4 次以上的模块边界上,ProtoBuf 的字段编号与保留机制能节省大量协调成本。

3.2 字段命名规范:跨语言一致性

字段名是契约可读性的最小单元。命名风格不统一会导致消费者困惑、代码生成器输出混乱、文档与实现脱节。

风格 示例 适用场景
snake_case user_id、account_status ProtoBuf 官方推荐、Python/Rust 生态、数据库列名
camelCase userId、accountStatus JavaScript/TypeScript 生态、大部分 REST JSON API
PascalCase UserId、AccountStatus 仅用于枚举类型名称或消息名称,不用于字段名
  • 固定规则:同一契约中只能使用一种命名风格,禁止混用。团队应在架构决策记录(ADR)中锁定风格。
  • ProtoBuf 字段命名:使用 snake_case,生成的 Go/Java/C++ 代码会自动转换为对应语言的惯用风格。
  • JSON 字段命名:建议使用 camelCase,若为历史兼容需保持 snake_case,应在契约注释中显式标注。
  • 编号稳定性:ProtoBuf 字段编号一旦发布即不可复用、不可变更旧编号含义;废弃字段使用 reserved 关键字。
// ProtoBuf 字段命名与 reserved 示例
message UserProfile {
  reserved 1, 4;          // 废弃字段编号
  reserved "old_field";   // 废弃字段名称

  string user_name = 2;
  int32  age = 3;
  string email = 5;       // 新字段使用递增编号
}
实践要点:字段名应自解释,避免缩写(usr_id → user_id),避免类型前缀(str_name → name),布尔字段使用肯定式(is_active 而非 not_inactive)。

3.3 枚举约定:默认值与未知值处理

枚举是模块边界中最易断裂的契约元素——新增枚举值若不被消费者识别,可能导致解析异常或静默错误。必须建立统一的枚举编写规范。

规则 JSON 实现 ProtoBuf 实现
枚举值命名 全大写 + 下划线:STATUS_ACTIVE 枚举类型名 + 全大写:STATUS_ACTIVE
默认值 不传时视为 null 或由业务层决定 必须定义 0 值枚举:STATUS_UNSPECIFIED = 0
未知值策略 服务端返回 UNKNOWN 或拒绝请求 ProtoBuf 3 默认将未知枚举值保留在二进制流中,可透传
新增枚举值 向后兼容,消费者需做 default 分支处理 追加新编号,避免复用旧编号;使用 reserved 标记废弃值
// ProtoBuf 枚举定义规范
enum EventType {
  EVENT_TYPE_UNSPECIFIED = 0;   // 必须存在,作为默认/未知占位
  EVENT_TYPE_LOGIN = 1;
  EVENT_TYPE_LOGOUT = 2;
  reserved 3;                   // 已废弃的枚举值编号
  reserved "EVENT_TYPE_DELETED";
  EVENT_TYPE_PURCHASE = 4;
}

// JSON 枚举表示(推荐全大写字符串)
// 请求: { "event_type": "EVENT_TYPE_LOGIN" }
// 响应: { "event_type": "EVENT_TYPE_PURCHASE" }
  • 禁止裸露枚举值:枚举值必须带有作用域前缀(如 STATUS_、EVENT_TYPE_),避免全局命名冲突。
  • 消费者防御:任何消费方代码必须对枚举值做 default 或 else 处理,不能假设枚举值集合固定。
  • 文档化枚举范围:在契约注释中列出当前所有枚举值及其含义,标注已废弃值及替代方案。
枚举演进建议:尽量将枚举定义为“开放集合”——即使 ProtoBuf 枚举默认封闭,也应在业务逻辑层预留未知值透传能力,避免因新增枚举值导致消费者崩溃。

3.4 选型与规范总览

下图展示了从序列化格式选型到字段命名、枚举约定的完整决策链路,以及各环节之间的依赖关系。

请求与响应格式规范框架 请求与响应格式规范框架 序列化格式选型 JSON(文本协议) ProtoBuf(二进制协议) 字段命名规范 枚举约定 文档化与最佳实践 统一规范是API互操作性的基石

3.5 契约示例与落地检查清单

以下是一组完整的契约片段,涵盖 JSON 请求/响应与 ProtoBuf IDL,展示字段命名与枚举规范的具体应用。

// ProtoBuf 完整契约示例
syntax = "proto3";
package user.v1;

import "google/protobuf/timestamp.proto";

enum UserStatus {
  USER_STATUS_UNSPECIFIED = 0;
  USER_STATUS_ACTIVE = 1;
  USER_STATUS_INACTIVE = 2;
  USER_STATUS_SUSPENDED = 3;
  reserved 4;
  reserved "USER_STATUS_DELETED";
}

message GetUserRequest {
  string user_id = 1;               // snake_case
}

message GetUserResponse {
  string user_id = 1;
  string display_name = 2;
  UserStatus status = 3;
  google.protobuf.Timestamp created_at = 4;
}
// 对应的 JSON 请求/响应(camelCase 命名)
// GET /v1/users/u-12345
// 响应:
{
  "userId": "u-12345",
  "displayName": "Alice",
  "status": "USER_STATUS_ACTIVE",
  "createdAt": "2024-06-01T10:00:00Z"
}
检查项 验收标准
格式选型决策记录 ADR 中明确记录选型理由、场景约束、混合策略(若有)
字段命名一致性 同一契约内 100% 遵循同一风格,无历史遗留混杂
枚举默认值 所有 ProtoBuf 枚举均包含 0 值 _UNSPECIFIED;JSON 枚举服务端做未知值兜底
废弃字段处理 ProtoBuf 使用 reserved 锁定编号和名称;JSON 字段用注释标记 deprecated
消费者防御代码 所有枚举消费方均包含 default 分支,不假设枚举值静止