数据契约的 Schema 定义:四种主流规范

上一章我们聊了数据契约是什么,说白了就是服务之间对数据格式的「书面约定」。那这个约定怎么写?用什么语法?这就涉及到 Schema 定义了。

我个人习惯把 Schema 理解成「数据的身份证」——它规定了数据长什么样、每个字段是什么类型、哪些必填、哪些可选。今天我们就来拆解四种主流的 Schema 定义方式:OpenAPI、JSON Schema、Protobuf 和 GraphQL Schema。

核心观点:没有「最好」的 Schema 语言,只有「最合适」的场景。选型的关键在于你的团队生态、性能要求和协作方式。

数据契约 Schema 定义方式全景 数据契约 Schema OpenAPI / Swagger JSON Schema Protobuf GraphQL Schema RESTful API 描述 HTTP 方法 + 路径 生态工具最丰富 纯数据校验标准 与传输协议无关 可嵌入 OpenAPI 强类型 + 二进制 向后兼容设计 高性能序列化 查询语言 + Schema 客户端按需取字段 类型系统强约束 适用:Web API、微服务网关 适用:数据校验、配置描述 适用:内部服务、高吞吐场景 适用:BFF、复杂数据聚合

1. OpenAPI / Swagger 规范

OpenAPI 是目前 Web API 领域事实上的标准。它最早叫 Swagger,后来捐给了 OpenAPI Initiative。说白了,它就是一套用 JSON 或 YAML 描述 RESTful 接口的规范。

一个典型的 OpenAPI 数据契约长这样:

openapi: 3.0.3
info:
  title: 用户服务契约
  version: 1.0.0
paths:
  /users/{userId}:
    get:
      summary: 获取用户信息
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
        - email
      properties:
        id:
          type: string
          description: 用户唯一标识
        name:
          type: string
          maxLength: 50
        email:
          type: string
          format: email
        age:
          type: integer
          minimum: 0
          maximum: 150

这里有几个关键点:

  • paths:定义 API 路径和 HTTP 方法
  • components/schemas:复用数据模型
  • $ref:引用其他 Schema 片段,避免重复
  • required:标记必填字段

我的经验:我在项目中遇到过团队把整个 API 文档写在一个几千行的 YAML 文件里,维护起来简直噩梦。建议用 $ref 拆分到多个文件,按模块组织。比如 user.yaml、order.yaml,主文件只做聚合。

2. JSON Schema

JSON Schema 是 OpenAPI 的底层校验引擎,但它本身可以独立使用。它不关心 HTTP 方法、不关心路径,只关心「数据长什么样」。

举个例子,一个订单数据契约:

{
  "$schema": "https://json-schema.org/draft-2020-12/schema",
  "$id": "https://api.example.com/schemas/order",
  "title": "订单契约",
  "type": "object",
  "properties": {
    "orderId": {
      "type": "string",
      "pattern": "^ORD-\\d{8}$"
    },
    "items": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "properties": {
          "sku": { "type": "string" },
          "quantity": { 
            "type": "integer", 
            "minimum": 1 
          },
          "price": { 
            "type": "number", 
            "exclusiveMinimum": 0 
          }
        },
        "required": ["sku", "quantity", "price"]
      }
    },
    "totalAmount": {
      "type": "number",
      "minimum": 0
    }
  },
  "required": ["orderId", "items", "totalAmount"]
}

JSON Schema 的优势在于:

  • 纯数据校验:不绑定任何传输协议
  • 丰富的约束:pattern、minimum、maxLength 等
  • 可组合:用 $defs 定义可复用的子 Schema
  • 跨语言:几乎所有语言都有校验库

避坑指南:我曾经在项目中用 JSON Schema 校验 Kafka 消息,结果发现 draft 版本不一致导致校验失败。团队用的 draft-07,但新引入的库只支持 draft-2020-12。所以一定要在契约头部显式声明 $schema 版本。

3. Protobuf 定义

Protobuf 是 Google 推出的序列化协议,在 gRPC 生态中广泛使用。它的 Schema 定义文件以 .proto 结尾,语法比 JSON Schema 更紧凑。

看一个例子:

syntax = "proto3";

package user.v1;

message User {
  string user_id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
  repeated string tags = 5;
  
  enum Status {
    STATUS_UNSPECIFIED = 0;
    STATUS_ACTIVE = 1;
    STATUS_INACTIVE = 2;
  }
  Status status = 6;
}

message GetUserRequest {
  string user_id = 1;
}

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
}

Protobuf 的几个核心特性:

特性 说明 我的评价
字段编号 每个字段有唯一数字编号(1-15 用 1 字节,16+ 用 2 字节) 编号一旦发布就不能改,否则破坏兼容性
强类型 int32、string、bool、enum 等 编译期就能发现类型错误
向后兼容 新增字段用 optional,删除字段用 reserved 这是 Protobuf 最让我放心的设计
代码生成 protoc 编译器生成 Java/Go/Python 等语言的代码 省去手写序列化/反序列化代码

关键认知:Protobuf 的 Schema 既是契约定义,也是代码生成器的输入。这意味着「契约即代码」——你改了 .proto 文件,重新编译,所有语言的桩代码自动更新。这在微服务间协作时效率极高。

4. GraphQL Schema

GraphQL 的 Schema 定义语言(SDL)是另一种风格。它既描述数据形状,也描述查询入口。

一个典型的 GraphQL Schema:

type User {
  id: ID!
  name: String!
  email: String!
  age: Int
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  createdAt: DateTime!
}

type Query {
  user(id: ID!): User
  users(limit: Int = 10): [User!]!
}

type Mutation {
  createUser(name: String!, email: String!): User!
}

GraphQL Schema 的特点:

  • 类型系统自带非空约束:! 表示非空,[Post!]! 表示数组本身非空且元素非空
  • 查询和变更分离:Query 读数据,Mutation 写数据
  • 关联关系自然表达:User 和 Post 通过字段直接关联
  • 客户端按需取字段:这是 GraphQL 最吸引人的地方

我的建议:如果你团队的前端对数据灵活性要求高,比如移动端需要不同字段组合,GraphQL 是很好的选择。但要注意,GraphQL 的 Schema 变更管理比 OpenAPI 复杂——因为客户端可以自由组合字段,你很难预判哪些字段被使用了。

如何选择?一张表说清楚

维度 OpenAPI JSON Schema Protobuf GraphQL
传输协议 HTTP/REST 任意 gRPC/HTTP HTTP/WebSocket
序列化格式 JSON JSON 二进制 JSON
性能 中等 中等 高 中等
学习曲线 低 低 中 中
工具生态 最丰富 丰富 丰富 丰富
变更管理 版本号 + 断点 手动管理 内置兼容规则 deprecation 机制
适用场景 对外 API、微服务网关 数据校验、配置描述 内部服务、高吞吐 BFF、数据聚合

重要提醒:不管你选哪种 Schema 语言,数据契约的核心不是语法,而是「约定」本身。我见过团队用 Protobuf 但字段编号随意分配,也见过用 OpenAPI 但 required 字段三天两头变。工具只是载体,契约意识才是根本。