← 文章 / 未分类
buf 7小时前 · 2026-09-22 22:02:06 · 0 阅读

Protobuf, JSON Schema, and OpenAPI

如果你正在使用 Protobuf,那么你已经拥有一个描述消息和服务的 schema。Protobuf 最为人熟知的功能是生成多种编程语言的类型、客户端和服务桩,但其插件系统还能输出更多内容,比如文档,以及将你的 schema 转换为其他格式。本文将介绍其中两种格式:JSON SchemaOpenAPI。这两种格式都能让你的原始 Protobuf schema 拓展到新场景。

两个插件让这一切成为可能:Buf 的 protoc-gen-jsonschema 和一个由我编写维护的社区插件 protoc-gen-connect-openapi。让我们来看看它们的产出以及如何将它们集成到项目中。

示例 schema

我们将使用一个小型库存服务,其中包含几条 Protovalidate 规则。一个产品包含一个特定格式的 SKU、2 到 100 个字符长度的名称、非负的库存数量,以及以十进制字符串表示的单价。

acme/inventory/v1/inventory.proto
syntax = "proto3";
 
package acme.inventory.v1;
 
import "buf/validate/validate.proto";
 
message Product {
  string sku = 1 [(buf.validate.field).string.pattern = "^[A-Z0-9-]+$"];
  string name = 2 [
    (buf.validate.field).string.min_len = 2,
    (buf.validate.field).string.max_len = 100
  ];
  int32 quantity = 3 [(buf.validate.field).int32.gte = 0];
  string unit_price = 4 [(buf.validate.field).string.pattern = "^[0-9]+\\.[0-9]{2}$"];
}
 
message GetProductRequest {
  string sku = 1 [(buf.validate.field).string.pattern = "^[A-Z0-9-]+$"];
}
 
message GetProductResponse {
  Product product = 1;
}
 
service InventoryService {
  rpc GetProduct(GetProductRequest) returns (GetProductResponse);
}

JSON Schema

protoc-gen-jsonschema 会为你的消息生成 JSON Schema(draft 2020-12)定义。以下是它针对 Product 生成的结果:

acme.inventory.v1.Product.jsonschema.json
{
  "$id": "acme.inventory.v1.Product.jsonschema.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": false,
  "properties": {
    "name": {
      "default": "",
      "maxLength": 100,
      "minLength": 2,
      "type": "string"
    },
    "quantity": {
      "anyOf": [
        {
          "exclusiveMaximum": 2147483648,
          "minimum": 0,
          "type": "integer"
        },
        {
          "pattern": "^-?[0-9]+$",
          "type": "string"
        }
      ],
      "default": 0
    },
    "sku": {
      "default": "",
      "pattern": "^[A-Z0-9-]+$",
      "type": "string"
    },
    "unitPrice": {
      "default": "",
      "pattern": "^[0-9]+\\.[0-9]{2}$",
      "type": "string"
    }
  },
  "title": "Product",
  "type": "object"
}

可以看到 Protovalidate 的规则都体现在了输出中:min_lenmax_len 变成了 minLengthmaxLength,SKU 的正则表达式变成了 patternquantity 的整数分支带有 minimum: 0 以及 int32 的上限。基于 Protobuf 的 JSON 映射anyOf 也允许用字符串表示整数。这是 .jsonschema.json 变体,所以字段使用 JSON 名称:Protobuf 中的 unit_price 在这里显示为 unitPrice

默认情况下,该插件会生成几个不同的文件,对应三个维度的组合:使用 Protobuf 还是 JSON 字段名、引用的 message 是内联还是拆分到独立文件、是否保留字符串编码整数这类替代表示形式。插件的 README 详细介绍了每种变体以及其他可用选项。

基于 JSON Schema 输出可以做的很多事情。你可以将 VS CodeJetBrains IDE 指向该 schema,从而获得字段名自动补全以及超范围值错误提示。这种用法最常见于编辑配置文件。你也可以用同一份 JSON Schema 文件在不可信数据入口处(如 webhook 或浏览器客户端)校验 payload。此外,你可以利用它来约束 GeminiChatGPT 等大语言模型的结构化输出,确保响应能被解析为你预期的数据格式。你还可以将同一份文件用于 表单生成器假数据生成器 以及 写入时进行校验的文档存储系统

OpenAPI

Connect 的单次调用就是带 JSON 请求体的 HTTP POST,而 OpenAPI 正擅长描述此类端点。

protoc-gen-connect-openapi 生成的就是 OpenAPI 描述。它由我编写并维护,属于个人项目而非 Buf 官方组件。它能生成 OpenAPI 3.1 文档,按 Connect 协议定义描述每个端点及相关类型。以下是我们为库存服务生成的输出示例(节选):

acme/inventory/v1/inventory.openapi.yaml
openapi: 3.1.0
info:
  title: acme.inventory.v1
paths:
  /acme.inventory/v1/InventoryService/GetProduct:
    post:
      operationId: acme.inventory.v1.InventoryService.GetProduct
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/acme.inventory.v1.GetProductRequest'
        required: true
      responses:
        default:
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/connect.error'
        "200":
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/acme.inventory.v1.GetProductResponse'
components:
  schemas:
    acme.inventory.v1.Product:
      type: object
      properties:
        sku:
          type: string
          pattern: ^[A-Z0-9-]+$
        name:
          type: string
          maxLength: 100
          minLength: 2
        quantity:
          type: integer
          minimum: 0
          format: int32
        unitPrice:
          type: string
          pattern: ^[0-9]+\.[0-9]{2}$
      additionalProperties: false

我截取了部分输出,因为其中还包含了一些实际有用但对本文显得冗余的标准选项和参数。例如,default 响应引用了 connect.error schema,该 schema 描述了任意端点都可能返回的 Connect 错误

OpenAPI 包含一些在 Protobuf schema 中通常不会定义的特性,比如服务器 URL 和认证方案。如果在本地运行该插件,可以使用 base=<file> 将手动编写的 OpenAPI 文件合并进去。它同样支持来自 google/gnostic 项目的 gnostic 注解,让你无需单独的配置文件,直接在 proto 中保留这些细节:文件级的服务器和安全方案、按 RPC 划分的操作设置,以及字段级的示例和格式等扩展信息。

拿到 OpenAPI spec 之后能做什么?你可以把它导入 ScalarSwagger UIRedoc 来生成文档站点,也可以交给 openapi-generator 这类工具,为 Connect 尚未直接支持的语言生成客户端。一些 API 网关产品还能根据 OpenAPI 规范在边缘拦截不匹配的流量,把那些低成本的爬虫和扫描器挡在后端之外。虽然我认为 Protobuf 是更简洁、更精确的 schema 格式,但很多个人和公司已经在 API 服务中深度集成了 OpenAPI,能够复用这些集成自然非常有价值。

三种运行插件的方式

这些插件实际上有三种运行方式,取决于你想在多大程度上自己维护依赖和构建流程。

作为本地插件

两个插件都是用 Go 写的,可以用 go install 安装:

go install github.com/bufbuild/protoschema-plugins/cmd/protoc-gen-jsonschema@latest
go install github.com/sudorandom/protoc-gen-connect-openapi@latest

把它们加进 buf.gen.yaml,然后运行 buf generate

buf.gen.yaml
version: v2
plugins:
  - local: protoc-gen-jsonschema
    out: gen/jsonschema
  - local: protoc-gen-connect-openapi
    out: gen/openapi

作为远程插件

这两个插件在 BSR 上也以远程插件的形式提供。把配置中的 local 换成 remote 后,Buf CLI 会把生成请求发给 BSR,由它在云端运行插件并返回生成的文件,本地什么都不用装:

buf.gen.yaml
version: v2
plugins:
  - remote: buf.build/bufbuild/protoschema-jsonschema
    out: gen/jsonschema
  - remote: buf.build/community/sudorandom-connect-openapi
    out: gen/openapi
$ buf generate
$ ls gen/openapi/acme/inventory/v1/
inventory.openapi.yaml

为了可复现构建,固定插件版本,例如 buf.build/bufbuild/protoschema-jsonschema:v0.5.0

作为生成的 SDK

对于已发布到 BSR 的模块,可以直接通过 URL 下载生成的文件。要获取 URL,打开该模块的 SDKs 选项卡,选择上述任一插件,然后复制 archive URL。例如,以下命令下载了 connectrpc/eliza 的 JSON Schema 和 OpenAPI 压缩包:

curl -fsSL -o eliza-jsonschema.zip https://buf.build/gen/archive/connectrpc/eliza/bufbuild/protoschema-jsonschema/latest.zip
curl -fsSL -o eliza-openapi.zip https://buf.build/gen/archive/connectrpc/eliza/community/sudorandom-connect-openapi/latest.zip

对于所有模块和插件,其 URL 都遵循以下模式:

https://buf.build/gen/archive/{owner}/{module}/{plugin_owner}/{plugin}/{reference}.zip<
原始来源: buf

评论 (0)