跳到主要内容

UCP — Schema 变体机制与文档工具链

本章给“要改 schema / 写规范文档”的人。它讲 UCP 一个很聪明的工程决定:不为每种操作各写一份 schema,而是一份 schema 配注解,按需派生变体——以及围绕这套注解建的文档构建宏与示例校验门禁。

1. 它要解决的小问题

同一个 checkout 对象,在不同场景下“哪些字段必填”是不同的:

  • 创建请求要 line_items,但不该带 id(还没创建)、不该带 status(由商家定)。
  • 完成请求要 payment,但不该带 line_items(已锁定)。
  • 响应要带 id/status/totals,但这些字段在请求里都该省略。

若为每种组合写一份 schema,会爆炸且难维护。UCP 的答案是注解。

2. 核心机制:ucp_request 注解

一份基础 schema 里,每个字段用 ucp_request 标注它在各操作下的角色。看 checkout 真实 schema(source/schemas/shopping/checkout.json:28-128):

{
"properties": {
"id": { "type": "string", "ucp_request": "omit" },
"status": { "type": "string", "ucp_request": "omit" },
"line_items": { "ucp_request": { "create": "required", "update": "required", "complete": "omit" } },
"payment": { "ucp_request": { "create": "optional", "update": "optional", "complete": "required" } }
}
}

读法:

  • "ucp_request": "omit" —— 该字段是纯响应字段,所有请求里都省略(如 idstatustotals)。
  • "ucp_request": { "create": "required", "complete": "omit" } —— 按操作分别规定角色。
  • 字段上甚至有 "ucp_request": "omit" 直接挂在 ucp 这个 $ref 旁(source/schemas/shopping/checkout.json:19-22),说明请求里整个 ucp 块都省。

这套注解也用在扩展里:discount 的 applied 字段标 ucp_request: "omit"(输出专用),而 discounts 字段在 create/update 是 optional、complete 时 omit。source/schemas/shopping/discount.json:93-141

注意区分两层注解:ucp_request请求方向的字段角色;而 ucp.json 里那套 platform_schema/business_schema/response_schema02 章)是注册表上下文的变体。两者正交。

3. 谁来解析注解:ucp-schema

注解本身不是 JSON Schema 标准——需要一个工具把它**解析(resolve)**成某个具体操作/方向的标准 schema。这个工具是外部 Rust crate ucp-schema不在本克隆内),本仓库通过子进程调用它。

本仓库两处真实调用,是理解其契约的最佳证据:

  • 文档构建期main.py_load_schema_variant 把页面上下文(io_type=request/response,operation_id)映射成 direction + operation,再调 ucp-schema resolve。映射逻辑见 main.py:281-301(如 op_id 含 "create" → operation="create",含 "complete" → "complete")。
  • 示例校验期scripts/validate_examples.pyresolve_schemaucp-schema resolve … --{direction} --op {op} --bundlescripts/validate_examples.py:633-668),validate_payloaducp-schema validatescripts/validate_examples.py:676-722)。

所以 ucp-schema 的职责(inferred,依据这两处调用方式而非其源码):吃带 ucp_request 注解的基础 schema + --op/--direction → 吐出剥离注解、字段必填性已按该操作定好的标准 JSON Schema。

4. 文档构建宏:把 schema 变成表格

UCP 的规范文档不手写字段表——而是用 MkDocs 宏从 schema 现拉。main.pydefine_env 注册了几个宏(main.py:21398210671255):

干什么定义处
schema_fields(entity, spec)渲染某实体的字段表(自动按页面 io_type 取变体)main.py:982-983
extension_schema_fields(ref, spec)渲染带 #/$defs/... 片段的实体表main.py:1067-1068
method_fields(op_id, file, spec)从 OpenAPI/OpenRPC 渲染某操作的请求/响应表main.py:1255-1256

在 checkout.md 里就能看到这些宏被直接写进 Markdown,例如 {{ schema_fields('checkout_resp', 'checkout') }}docs/specification/checkout.md:605)、{{ method_fields('create_checkout', 'rest.openapi.json', 'checkout') }}docs/specification/checkout.md:633)。构建时它们被替换成表格。

这意味着:改 schema,文档表格自动更新——schema 是单一事实源。

5. 示例校验门禁:schema 漂移直接断 CI

UCP 把规范里每个 ```json 代码块都拿去对 schema 校验。机制是给每个块前加一行 HTML 注释注解(不渲染):

<!-- ucp:example schema=PATH [op=OP] [direction=DIR] [extract=JSONPATH] [target=JSONPATH] [def=NAME] -->
<!-- ucp:example skip reason="..." -->

未加注解的 json 块直接判失败。来源:docs/documentation/schema-authoring.md:482-526、校验器实现 scripts/validate_examples.py 模块 docstring §15-108。

校验器是个三层管线(scripts/validate_examples.py:74-88process_block §875-1067):

Layer 1 表面语法 作者写的“增强 JSON”
(行注释 //、{{ ucp_version }}、HTTP 信封、省略号 ... 标记)
│ reduce_to_canonical_json:四步纯文本归约

Layer 2 规范 JSON json.loads 能解析(省略号留作哨兵)


Layer 3 语义校验
① extract= 选子树 ② 记录被省略路径 ③ 深合并进 scaffold(已知合法夹具)
④ coverage 走查:每个必填字段要么出现、要么被省略号显式确认
⑤ ucp-schema validate;被省略路径上的报错被抑制

几个值得学的细节:

  • 省略号有语义。 [ ... ]/{ ... } 表示“非空容器、内容省略”,coverage 仍核验该字段被确认存在,但不对其值报错。docs/documentation/schema-authoring.md:600-639scripts/validate_examples.py:374-416
  • scaffold(夹具)补全。 示例只展示关注字段,校验器把它深合并进一个已知合法的 scaffold(scripts/scaffolds/*.json)再校验,所以作者不必写全。scripts/validate_examples.py:497-514783-806
  • 未知注解属性直接拒。 打错 shema= 会被 _KNOWN_ATTRS 拦下而非静默忽略——typo 守卫。scripts/validate_examples.py:135-171
  • skip 理由要可 grep。 不能校验的块用 skip reason="..." 并给精确分类(如 "A2A transport binding"),用来追踪“还没覆盖什么”。docs/documentation/schema-authoring.md:652-672

触发面三层(docs/documentation/schema-authoring.md:786-800):pre-commit 改了的 doc 只校验改动文件;改了 source/schemas/ 或校验器本身则跑全量(因为一处 schema 改动可能波及多篇文档的示例);CI 无条件全量兜底。

6. 巧妙之处(可借鉴)

  • 巧:一份 schema 派生多变体。ucp_request 注解避免了 N 份近似 schema 的维护噩梦,且让“某字段在创建时必填、完成时省略”这种规则就近声明在字段上,可读性极高。source/schemas/shopping/checkout.json
  • 巧:文档即 schema 的投影。 宏让字段表永不和 schema 脱节;校验器让示例永不和 schema 脱节——“规范漂移”从“可能悄悄发生”变成“直接断 CI”。main.pyscripts/validate_examples.py
  • 巧:增强 JSON 但在线格式仍严格。 作者写带 // 注释和省略号的“增强 JSON”图清楚,校验器先归约成严格 RFC 8259 JSON 再校验——在线格式保持诚实。scripts/validate_examples.py:46-72
  • 巧:把校验内务藏起来。 注解活在 HTML 注释里、scaffold 和校验 schema 在仓库内务目录,规范读者只看到协议散文和 JSON。docs/documentation/schema-authoring.md:719-745

7. 边界

  • 校验器的 // 注释剥离是按行近似的:字符串里含转义反斜杠后跟 // 会误判(已知边界,当前语料无触发)。scripts/validate_examples.py:90-98
  • 只认一个模板变量 {{ ucp_version }};其余 {{ name }} 会留进 json.loads 并报错(故意的)。scripts/validate_examples.py:302-308
  • 不支持内层省略号 [a, ..., b]、尾逗号、块注释、JSON5。scripts/validate_examples.py:56-60

8. 代码地图

主题文件符号
ucp_request 注解(真值)source/schemas/shopping/checkout.jsonproperties.*.ucp_request
注解约定(作者契约)docs/documentation/schema-authoring.md“Schema Variants”、“ucp_request”
变体解析(调 ucp-schema)main.py_load_schema_variant_resolve_schema
文档宏main.pydefine_envschema_fieldsextension_schema_fieldsmethod_fields
示例校验管线scripts/validate_examples.pyreduce_to_canonical_jsonstrip_ellipsischeck_coverageprocess_block
注解解析 / typo 守卫scripts/validate_examples.pyparse_annotation_KNOWN_ATTRS
scaffold 夹具scripts/scaffolds/*_request_create.json
CI 门禁docs/documentation/schema-authoring.md“What runs automatically” §786-814