Schema 定义入门:三种主流方案

聊数据契约,绕不开一个基础问题——Schema 到底怎么写?

我见过不少团队,上来就选 Protobuf,结果业务逻辑还没理清,先被编译流程折腾得够呛。也见过用 JSON Schema 的,文档写得比代码还长,最后没人维护。

说白了,选哪种 Schema 语言,取决于你的场景。这一章,我带你把 Avro、JSON Schema、Protobuf 三种方案过一遍。不搞虚的,直接看代码、看差异、看坑。

核心观点:Schema 不是文档,是契约的骨架。选对格式,演化才可能平滑。

三种 Schema 定义方案对比 Avro JSON 格式定义 动态类型 Schema Registry 原生支持 适合:Kafka 消息 演化:强兼容模式 JSON Schema JSON 格式定义 纯描述性 无序列化绑定 适合:API 校验、文档 演化:手动管理 Protobuf .proto 文件定义 强类型 + 代码生成 二进制序列化 适合:RPC、微服务 演化:字段编号驱动 选择建议:消息队列用 Avro,API 网关用 JSON Schema,微服务 RPC 用 Protobuf

Avro Schema:Kafka 生态的标配

Avro 是 Apache 旗下的序列化框架。它的 Schema 用 JSON 写,这一点很讨喜——你不需要学新的 DSL。

我个人习惯把 Avro 用在 Kafka 消息上。为什么?因为它和 Schema Registry 配合得天衣无缝。生产者发消息时带上 Schema ID,消费者自动拉取,演化规则内置。

看一个简单的例子:

{
  "type": "record",
  "name": "User",
  "namespace": "com.example",
  "fields": [
    {"name": "id", "type": "int"},
    {"name": "name", "type": "string"},
    {"name": "email", "type": ["null", "string"], "default": null}
  ]
}

注意 email 字段。我用了联合类型 ["null", "string"],还给了默认值 null。这是 Avro 演化的关键——新增字段必须有默认值,否则老数据读不了新 Schema。

小技巧:Avro 的 default 不只是给个值,它还决定了演化方向。有默认值的字段可以安全添加,删除字段则要谨慎——老数据可能还依赖它。

JSON Schema:API 契约的瑞士军刀

JSON Schema 不负责序列化,它只做一件事——描述数据长什么样。你想想看,REST API 的请求体、配置文件、事件通知……只要数据是 JSON,就能用 JSON Schema 约束。

我在项目中遇到过最典型的场景:前端和后端各写一套校验逻辑,结果对不上。后来统一用 JSON Schema,前端用 ajv,后端用 jsonschema 库,问题一次性解决。

来个例子:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "userId": {
      "type": "integer",
      "minimum": 1
    },
    "userName": {
      "type": "string",
      "maxLength": 50
    },
    "email": {
      "type": "string",
      "format": "email"
    }
  },
  "required": ["userId", "userName"]
}

这里有个坑——required 数组。新增字段时,如果把它放进 required,老客户端直接报错。我建议新字段先不加 required,等所有消费者都升级了再补上。

注意:JSON Schema 本身不提供演化机制。字段增减、类型变更全靠人工协调。所以它更适合内部 API 或文档生成,不适合高频演化的消息系统。

Protobuf:微服务通信的工业标准

Protobuf 是 Google 的序列化协议。它的 Schema 写在 .proto 文件里,然后通过编译器生成各语言的代码。强类型、二进制编码、性能好——微服务 RPC 场景几乎绕不开它。

我记得第一次用 Protobuf 时,最不习惯的是字段编号。每个字段都得手动标一个数字,而且不能重复、不能改。后来才明白,编号才是 Protobuf 演化的核心。

看代码:

syntax = "proto3";

package com.example;

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  // 新增字段,编号 4
  string phone = 4;
}

演化规则很简单:

  • 新增字段用新编号,老客户端忽略它
  • 废弃字段用 reserved 保留编号,防止误用
  • 永远不要改已有字段的类型和编号

关键点:Protobuf 的 reserved 关键字很多人忽略。我曾经接手过一个项目,有人删了字段没保留编号,后来新加字段用了同一个编号,结果线上数据解析全乱套。教训深刻。

三种方案怎么选?

直接给结论,不绕弯子:

维度 Avro JSON Schema Protobuf
定义格式 JSON JSON .proto 文件
序列化 二进制 无(纯描述) 二进制
代码生成 可选 无 强制
演化支持 强(内置兼容规则) 弱(人工管理) 中(编号驱动)
典型场景 Kafka 消息、数据管道 API 校验、配置文件 gRPC、微服务 RPC

选型建议:

  • 如果你的数据流经过 Kafka,优先 Avro。Schema Registry 的兼容性检查能省掉你大量排错时间。
  • 如果只是 REST API 的请求体校验,JSON Schema 就够了。别为了用 Protobuf 而引入编译流程。
  • 如果是微服务之间的 RPC 调用,Protobuf 是默认选项。性能好、工具链成熟、跨语言支持完善。

我的经验:同一个项目里,三种 Schema 可以共存。Kafka 消息用 Avro,对外 API 用 JSON Schema,内部服务用 Protobuf。关键是统一管理好 Schema 文件,别让它们散落在各个仓库里。