2. 数据契约基础:什么是数据契约、契约的组成要素

在微服务架构与 API 网关迁移场景中,数据契约是服务间通信的“法律条文”。它明确定义了:一个 API 在给定输入(请求)下,应当返回什么样的输出(响应),以及这些数据的形状、类型和约束。没有数据契约,迁移后的冒烟检查将失去比对基准。

2.1 什么是数据契约

数据契约是 API 提供方与消费方之间,关于数据交换格式与语义的显式约定。它通常以机器可读的规范文件(如 OpenAPI 3.x、JSON Schema、Protobuf IDL)为载体,核心目标包括:

  • 解耦:服务提供方与消费方不依赖同一代码库,只依赖契约。
  • 可验证:迁移后的响应可对照契约进行自动化断言。
  • 可演进:通过版本化契约管理向后兼容变更。
关键认知:数据契约不是文档,而是可执行的测试基线。在 OpenAPI 迁移场景中,旧网关的流量回放结果必须与新网关的响应同时通过同一份契约校验,才能判定迁移成功。
数据契约在 OpenAPI 迁移中的核心位置 数据契约在 OpenAPI 迁移中的核心位置 旧网关(流量回放) 新网关(迁移目标) 数据契约 (OpenAPI / JSON Schema) 冒烟检查:两路响应均通过契约校验 → 迁移成功

2.2 契约的组成要素

一份完整的数据契约由以下三大要素构成,缺一不可。在冒烟检查中,每个要素都需要被显式验证。

2.2.1 请求结构(Request Structure)

定义消费方如何调用 API,包括:

  • HTTP 方法 + 路径:如 GET /users/{userId}
  • 路径参数:如 userId 的类型、格式
  • 查询参数:如 ?page=1&size=20 的默认值与约束
  • 请求头:如 Authorization、Content-Type
  • 请求体(针对 POST/PUT):JSON 对象的嵌套结构

2.2.2 响应结构(Response Structure)

定义提供方返回的数据形状,通常按 HTTP 状态码分组:

  • 200 OK:成功响应的主体结构
  • 4xx/5xx:错误响应的统一格式(如 { "code": int, "message": string })
  • 响应头:如 X-Request-Id、RateLimit-Remaining

2.2.3 字段类型与约束(Field Types & Constraints)

这是契约中最细粒度的校验单元,也是冒烟检查最容易遗漏的部分。下表列出常见字段约束及其在 OpenAPI 中的表示方式:

约束类别 OpenAPI 关键字 示例 冒烟检查要点
数据类型 type type: integer 响应中字段实际类型是否匹配
必填性 required required: [id, name] 必填字段是否缺失或为 null
取值范围 minimum / maximum minimum: 1, maximum: 100 数值是否越界
字符串长度 minLength / maxLength maxLength: 255 字符串是否超长
枚举值 enum enum: [pending, active, closed] 字段值是否在枚举列表内
格式模式 format / pattern format: email / pattern: '^\d{11}$' 格式正则是否通过
数组约束 minItems / maxItems minItems: 1 数组是否为空或超量
嵌套对象 $ref / properties $ref: '#/components/schemas/Address' 嵌套结构是否完整
实践建议:在冒烟检查脚本中,不要只校验顶层字段类型。深度嵌套对象中的 required 和 enum 约束是迁移后最容易“静默断裂”的地方——旧网关可能宽松地返回了额外字段,而新网关严格按契约裁剪后,消费方可能因缺少预期字段而崩溃。
数据契约组成要素结构图 数据契约组成要素结构图 数据契约 ① 请求结构 ② 响应结构 ③ 字段类型与约束 路径参数 · 查询参数 · 请求头 · 请求体 状态码分组 · 响应头 · 响应体结构 类型 · 必填 · 范围 · 枚举 · 格式 · 嵌套 冒烟检查必须覆盖全部三个要素 仅校验响应状态码而不检查字段约束,是迁移后故障的第一大原因

2.3 契约的表示形式:OpenAPI 示例

以下是一个简化的 OpenAPI 3.x 数据契约片段,展示了请求、响应与字段约束的完整定义:

openapi: 3.0.3
info:
  title: 用户服务 API
  version: 1.0.0
paths:
  /users/{userId}:
    get:
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: 成功返回用户信息
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: 用户不存在
          content:
            application/json:
              schema:
                type: object
                required: [code, message]
                properties:
                  code:
                    type: integer
                    enum: [40401, 40402]
                  message:
                    type: string
                    maxLength: 200
components:
  schemas:
    User:
      type: object
      required: [id, name, email, status]
      properties:
        id:
          type: integer
          minimum: 1
        name:
          type: string
          maxLength: 100
        email:
          type: string
          format: email
        status:
          type: string
          enum: [active, inactive, suspended]
        createdAt:
          type: string
          format: date-time
常见陷阱:迁移过程中,旧网关可能因为历史原因返回了契约中未定义的字段(如 extraInfo),而新网关严格按契约裁剪后,消费方如果依赖该字段将直接报错。因此,冒烟检查不仅要校验“契约中定义的字段是否存在”,还要校验“响应中不应出现未定义的字段”。

2.4 冒烟检查中的契约验证策略

基于上述组成要素,冒烟检查脚本应至少包含以下 5 个验证步骤:

  1. 结构完整性:响应 JSON 的顶层 key 集合是否与契约一致。
  2. 类型匹配:每个字段的运行时类型是否与 type 声明一致。
  3. 约束边界:数值是否在 minimum/maximum 内,字符串是否超长。
  4. 枚举合法性:枚举字段的值是否在允许列表中。
  5. 嵌套递归:对 $ref 引用的子对象递归执行上述检查。