@composio/json-schema-to-zod 的 Zod v3 兼容性端到端测试指南
【免费下载链接】composioComposio powers 1000+ toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action.项目地址: https://gitcode.com/GitHub_Trending/co/composio
在 Composio 的 monorepo 中,@composio/json-schema-to-zod负责把 JSON Schema(draft 4+)在运行时转换为 Zod 校验对象,是工具参数 schema 与各 Agent 框架(OpenAI、Claude、LangChain 等)之间最重要的桥接层之一。由于生态中既有项目仍大量依赖 Zod v3,而新项目已开始迁移到 Zod v4,该包必须同时兼容两个大版本。本文以 ts/e2e-tests/runtimes/node/json-schema-to-zod-v3/README.md 为骨架,结合测试用例 e2e.test.ts 与转换器源码 parse-object.ts,完整讲解这套 Zod v3 兼容性端到端测试套件的目标、运行方式、覆盖场景与底层实现原理。读完本文,你将掌握如何在多 Node.js 版本下验证 schema 转换的正确性,理解additionalProperties在 JSON Schema → Zod → JSON Schema 往返过程中的语义保持策略,并能复现整套测试。
为什么需要 Zod v3 兼容性验证
@composio/json-schema-to-zod包必须同时支持 Zod v3 与 Zod v4 两个大版本。Zod v3 与 v4 在 API 表面、类型系统(如深度类型实例化限制)和行为细节上存在差异,仅做单元测试无法覆盖"在真实运行时环境中、与zod-to-json-schema等生态库协作"的场景。因此 Composio 在端到端测试目录下单独建立了一个json-schema-to-zod-v3测试套件,专门用于验证该包在zod@3.25.76下的表现。
该套件明确验证四类核心能力(引自 README.md):
- JSON Schema 到 Zod 的转换在 Zod v3 下正常工作;
- 所有 schema 类型(string、object、array、anyOf 等)都能正确转换;
- 往返转换(JSON Schema → Zod → JSON Schema)保持语义不变;
additionalProperties的处理行为正确。
测试套件本身位于 monorepo 的ts/e2e-tests/runtimes/node/目录下,属于"按运行时 + 依赖组合"组织的端到端测试矩阵。其包名为@e2e-tests/node-json-schema-to-zod-v3(见 package.json)。
测试套件结构:它覆盖了哪些场景
README 中的测试清单表格完整对应 e2e.test.ts 中的两个describe块("Basic functionality" 与 "Round-trip conversion"),覆盖场景如下:
| 测试 | 说明 |
|---|---|
| 基础 string schema | 转换{ type: 'string' }并执行校验 |
| object schema | 必填字段、嵌套属性、校验约束 |
| array schema | 带校验的强类型数组元素 |
| email 格式 | 对字符串做 email format 校验 |
| 嵌套 schema | 复杂嵌套对象与数组 |
| anyOf schema | 联合类型转换 |
| 往返转换 | JSON Schema → Zod → JSON Schema 保持additionalProperties语义 |
以上 6 项基础能力测试对应 e2e.test.ts,往返转换测试对应 e2e.test.ts。
测试环境与运行方式
直接在 Bun 中运行,无需 Docker fixtures
与其他需要 fixture 文件的 e2e 套件不同,本套件直接在 Bun 中运行,测试文件从 monorepo workspace 中导入两个关键依赖:
import { jsonSchemaToZod, type JsonSchema } from '@composio/json-schema-to-zod'; import zodToJsonSchema from 'zod-to-json-schema';(见 e2e.test.ts)
依赖版本在 package.json 中锁定:
@composio/json-schema-to-zod:workspace:*(始终使用 monorepo 当前源码);zod:3.25.76(被验证的目标版本);zod-to-json-schema:catalog:(由 workspace 目录统一管理版本)。
测试框架与入口
测试使用bun:test断言(describe/it/expect),并通过@e2e-tests/utils提供的e2e()辅助函数接入整套隔离运行基础设施:
e2e(import.meta.url, { versions: { node: ['22.22.3', '24.17.0', '25.9.0'] }, defineTests: () => { // describe / it / expect 断言块 }, });(见 e2e.test.ts)
e2e()的实现位于 ts/e2e-tests/_utils/src/e2e.ts:它会校验传入的import.meta.url是合法file://URL,然后从调用者所在目录推断出相对于仓库根的 cwd 与 suiteName,再交给底层的runE2E()在 Docker 容器内依次按各运行时版本执行。
隔离工具:Docker + 多版本 Node.js
README 明确说明隔离工具为Docker,覆盖三个 Node.js 版本:22.22.3、24.17.0、25.9.0。这三个版本是@e2e-tests/utils中预定义的 well-known 版本(见 ts/e2e-tests/_utils/README.md 的 "Well-Known Node Versions" 一节)。运行基础设施会:
- 为每个 Node 版本构建对应的 Docker 镜像;
- 在隔离容器内执行测试命令;
- 按版本顺序串行运行;
- 对容器卷做 best-effort 清理。
运行命令
pnpm test:e2e(引自 README.md 的 "Running" 一节)
在套件目录内,package.json 还提供了两个等价脚本:test:e2e与test:e2e:node,均执行bun test e2e.test.ts;另有typecheck脚本(tsc --noEmit)用于静态类型检查。对应的 tsconfig.json 采用es2022目标与moduleResolution: bundler。
运行结果会写入DEBUG.log,按 Node 版本分组输出各阶段的 setup 命令、stdout/stderr 与通过/失败汇总(该机制定义于 ts/e2e-tests/_utils/README.md)。
基础转换测试逐项拆解
下面逐一展开 e2e.test.ts 中的 6 个基础能力用例,每个用例都遵循同一模式:定义 JSON Schema → 调用jsonSchemaToZod→ 用.parse()验证合法数据通过、非法数据抛错。
1. 基础 string schema
const schema: JsonSchema = { type: 'string' }; const zodSchema = jsonSchemaToZod(schema); expect(zodSchema.parse('hello')).toBe('hello'); expect(() => zodSchema.parse(123)).toThrow();(见 e2e.test.ts)
最简场景:字符串通过、数字抛错,验证了最基础的type映射。
2. 带校验约束的 object schema
const schema: JsonSchema = { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number', minimum: 0 }, }, required: ['name'], }; const zodSchema = jsonSchemaToZod(schema); expect(zodSchema.parse({ name: 'John', age: 30 })).toEqual({ name: 'John', age: 30 }); expect(zodSchema.parse({ name: 'John' })).toEqual({ name: 'John' }); expect(() => zodSchema.parse({ age: 30 })).toThrow();(见 e2e.test.ts)
该用例验证三点:必填字段(required中的name缺失即抛错)、数值约束(minimum: 0被转换为 Zod 的.min()校验)、非必填字段可省略。
3. array schema
const schema: JsonSchema = { type: 'array', items: { type: 'string' }, }; const zodSchema = jsonSchemaToZod(schema); expect(zodSchema.parse(['one', 'two', 'three'])).toEqual(['one', 'two', 'three']); expect(() => zodSchema.parse(['one', 2])).toThrow();(见 e2e.test.ts)
验证数组元素类型被强约束:items声明为 string 后,混入数字即失败。
4. email 格式校验
const schema: JsonSchema = { type: 'string', format: 'email', }; const zodSchema = jsonSchemaToZod(schema); expect(zodSchema.parse('test@example.com')).toBe('test@example.com'); expect(() => zodSchema.parse('invalid-email')).toThrow();(见 e2e.test.ts)
format: 'email'被转换为 Zod 的z.string().email(),合法邮箱通过、非法字符串抛错。
5. 复杂嵌套 schema
const schema: JsonSchema = { type: 'object', properties: { user: { type: 'object', properties: { name: { type: 'string' }, contacts: { type: 'array', items: { type: 'object', properties: { type: { type: 'string' }, value: { type: 'string' }, }, required: ['type', 'value'], }, }, }, required: ['name'], }, }, required: ['user'], };(见 e2e.test.ts)
构造了「对象 → 对象 → 数组 → 对象」的四层嵌套,并在每一层标注required。合法的数据形如:
const validData = { user: { name: 'Jane Doe', contacts: [ { type: 'email', value: 'jane@example.com' }, { type: 'phone', value: '555-1234' }, ], }, }; expect(zodSchema.parse(validData)).toEqual(validData);(见 e2e.test.ts)
6. anyOf 联合类型
const schema: JsonSchema = { anyOf: [{ type: 'string' }, { type: 'number' }], }; const zodSchema = jsonSchemaToZod(schema); expect(zodSchema.parse('hello')).toBe('hello'); expect(zodSchema.parse(42)).toBe(42); expect(() => zodSchema.parse(true)).toThrow();(见 e2e.test.ts)
anyOf被转换为 Zod 联合类型(union):字符串、数字各自通过,布尔值不在联合内因此抛错。
往返转换:additionalProperties 语义的完整验证
往返转换(round-trip)是这套测试最有价值的部分。它验证的不只是"转出来能用",而是JSON Schema → Zod → JSON Schema(zod-to-json-schema)的完整链条上,additionalProperties的语义不丢失。这直接关系到工具 schema 的严格/宽松行为,例如 OpenAI 等 Provider 对未知参数的处理策略。
用例 1:空对象 +additionalProperties: true
const schema: JsonSchema = { type: 'object', additionalProperties: true, }; const zodSchema = jsonSchemaToZod(schema); const convertedBack = zodToJsonSchema(zodSchema, { target: 'jsonSchema7' }); expect(convertedBack.additionalProperties).toBe(true); expect(convertedBack.type).toBe('object'); expect(zodSchema.parse({})).toEqual({}); expect(zodSchema.parse({ any: 'value', number: 123 })).toEqual({ any: 'value', number: 123 });(见 e2e.test.ts)
注意测试中的两处@ts-expect-error:由于zod-to-json-schema返回类型与 Zod v3 深度类型实例化的限制,测试需要显式放宽类型,这本身就是 Zod v3 环境下真实存在的类型兼容问题。
用例 2:含命名属性 +additionalProperties: true
const schema: JsonSchema = { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' } }, required: ['name'], additionalProperties: true, }; const zodSchema = jsonSchemaToZod(schema); const convertedBack = zodToJsonSchema(zodSchema, { target: 'jsonSchema7' }); expect(convertedBack.additionalProperties).toBe(true); expect(convertedBack.properties).toBeDefined(); expect(convertedBack.required).toEqual(['name']); expect(zodSchema.parse({ name: 'John', age: 30, extra: 'field' })).toEqual({ name: 'John', age: 30, extra: 'field', });(见 e2e.test.ts)
additionalProperties: true意味着未知字段应当被允许且原样保留,往返后仍为true,且命名属性与required均被保留。
用例 3:空对象 +additionalProperties: false
const schema: JsonSchema = { type: 'object', additionalProperties: false, }; const zodSchema = jsonSchemaToZod(schema); const convertedBack = zodToJsonSchema(zodSchema, { target: 'jsonSchema7' }); const additionalPropsValid = convertedBack.additionalProperties === false || (convertedBack.not && typeof convertedBack.not === 'object'); expect(additionalPropsValid).toBe(true); expect(convertedBack.type).toBe('object'); expect(zodSchema.parse({})).toEqual({}); expect(() => zodSchema.parse({ extra: 'field' })).toThrow();(见 e2e.test.ts)
这是一个很能体现语义保持策略的用例:测试注释明确说明,zod-to-json-schema可能把z.object({}).strict()转换为{ not: {} }——这在语义上等价于additionalProperties: false。因此断言允许两种合法形态之一,既验证行为正确,又不绑定实现细节。
用例 4:含命名属性 +additionalProperties: false
const schema: JsonSchema = { type: 'object', properties: { name: { type: 'string' } }, additionalProperties: false, }; const zodSchema = jsonSchemaToZod(schema); const convertedBack = zodToJsonSchema(zodSchema, { target: 'jsonSchema7' }); expect(convertedBack.additionalProperties).toBe(false); expect(convertedBack.properties).toBeDefined(); expect(zodSchema.parse({ name: 'John' })).toEqual({ name: 'John' }); expect(() => zodSchema.parse({ name: 'John', extra: 'field' })).toThrow();(见 e2e.test.ts)
命名属性存在时,additionalProperties: false精确地往返为false,多余字段被拒绝。
用例 5:additionalProperties带类型 schema
const schema: JsonSchema = { type: 'object', properties: { name: { type: 'string' } }, additionalProperties: { type: 'number' }, }; const zodSchema = jsonSchemaToZod(schema); const convertedBack = zodToJsonSchema(zodSchema, { target: 'jsonSchema7' }); expect(convertedBack.additionalProperties).toEqual({ type: 'number' }); expect(convertedBack.properties).toBeDefined(); expect(zodSchema.parse({ name: 'John', age: 30 })).toEqual({ name: 'John', age: 30 }); expect(() => zodSchema.parse({ name: 'John', extra: 'field' })).toThrow();(见 e2e.test.ts)
additionalProperties不仅可以是布尔值,还可以是完整 schema。这里未知键age因符合{ type: 'number' }而通过,extra: 'field'因不是数字而被拒绝,且往返后additionalProperties精确恢复为{ type: 'number' }。
用例 6:嵌套对象中不同的 additionalProperties 组合
const schema: JsonSchema = { type: 'object', properties: { strictChild: { type: 'object', properties: { name: { type: 'string' } }, additionalProperties: false, }, flexibleChild: { type: 'object', properties: { age: { type: 'number' } }, additionalProperties: true, }, }, additionalProperties: { type: 'string' }, }; const zodSchema = jsonSchemaToZod(schema); const convertedBack = zodToJsonSchema(zodSchema, { target: 'jsonSchema7' }); expect(convertedBack.additionalProperties).toEqual({ type: 'string' }); const convertedProperties = convertedBack.properties as Record<string, unknown>; expect(convertedProperties?.strictChild).toBeDefined(); expect(convertedProperties?.flexibleChild).toBeDefined();(见 e2e.test.ts)
最复杂的场景:同一层additionalProperties: { type: 'string' }、子对象 AadditionalProperties: false(严格)、子对象 BadditionalProperties: true(宽松),三个层级的策略互不影响。合法数据要求extraString是字符串,而extraNumber: 123会被拒绝;strictChild内的额外键会被拒绝,flexibleChild内的额外键则被允许(见 e2e.test.ts)。
源码级实现:additionalProperties 是如何映射到 Zod 的
往返测试之所以能成立,根源在于转换器 parse-object.ts 对additionalProperties的精细处理。该文件是@composio/json-schema-to-zod包的核心实现之一(包目录见 ts/packages/json-schema-to-zod)。
jsonSchemaToZod的入口在 json-schema-to-zod.ts:它调用parseSchema递归转换,并在需要整 schema 校验时(requiresWholeSchemaValidation)用withWholeSchemaValidation包裹结果。同文件还导出jsonSchemaToZodShape(json-schema-to-zod.ts),用于需要ZodRawShape而非完整ZodType的场景(例如 Claude Agent SDK 的tool()函数)。
在 parse-object.ts 中,additionalProperties的决策逻辑可归纳为:
| 输入条件 | Zod 输出 |
|---|---|
有命名属性 +additionalProperties: false(解析为ZodNever) | propertiesSchema.strict() |
有命名属性 +additionalProperties: true(显式) | propertiesSchema.passthrough() |
有命名属性 +additionalProperties为 schema | propertiesSchema.catchall(schema) |
有命名属性 + 省略additionalProperties | propertiesSchema.strict()(对工具输入保持严格) |
无命名属性 +additionalProperties: false | z.object({}).strict() |
无命名属性 +additionalProperties: true(显式) | z.object({}).passthrough() |
无命名属性 +additionalProperties为 schema | z.record(schema) |
无命名属性 + 省略additionalProperties | z.object({}).passthrough()(开放式对象) |
几个值得注意的实现细节(均可从源码注释确认):
- 省略时的默认严格:带命名属性却省略
additionalProperties时,转换器故意生成.strict(),而不是遵循 JSON Schema 原生"默认允许未知键"的宽松语义。源码注释(parse-object.ts)说明这是为了工具输入(tool inputs)场景而刻意收紧,防止模型传入拼写错误的参数被静默吞掉。 - 空对象回退的历史教训:
parseObjectProperties的注释(parse-object.ts)记录了此前z.object({})回退导致的缺陷——把{ type: 'object' }错误地塌缩成严格空对象,会破坏 OpenAI 等 Provider 的响应处理。因此无命名属性的对象现在采用开放式策略,且 OpenAI Provider 测试套件被纳入该包的验证范围。 patternProperties的动态键处理:当 schema 含patternProperties时,走parseDynamicKeyObject(parse-object.ts),每个键只路由到匹配的模式或additionalProperties,避免"键通过满足一个不相关模式而漏过自身模式"的问题;未匹配且被拒绝的键会以unrecognized_keys错误列出。
正是这套决策树,保证了 e2e.test.ts 中 6 个往返用例的断言全部成立。
版本演进与测试矩阵定位
该套件与@composio/json-schema-to-zod的发布保持同步。从 CHANGELOG.md 可见:0.0.1跟随@composio/json-schema-to-zod@0.3.1,0.0.2跟随@composio/json-schema-to-zod@0.3.2。这意味着每当转换器包升级,本套件都会随 monorepo 的 changeset 机制联动更新,确保持续在真实 Zod v3 环境回归。
在整个 e2e 矩阵中,本套件属于"按依赖组合划分"的兼容性系列——例如 ts/e2e-tests/_utils/README.md 的 DEBUG.log 示例中还展示了同系列的openai-zod4-compat套件。它们共享同一套e2e()基础设施:多版本运行时隔离、setup/fixture 两阶段执行、按版本分组的DEBUG.log输出(包含各阶段的命令、stdout/stderr、耗时与 PASS/FAIL 汇总)。如果你需要为其他依赖组合新增类似的兼容性验证,直接在ts/e2e-tests/runtimes/node/下仿照本套件的目录结构(e2e.test.ts+package.json+tsconfig.json+README.md)新建套件即可,e2e()会自动推断工作目录与套件名。
总结:如何复现与扩展这套验证
一句话总结这套测试的价值:它把"JSON Schema 转换器与 Zod 生态的兼容性"从隐式假设变成了显式、可重复、跨 Node 版本的机器验证。
本地复现步骤:
- 在仓库根目录安装依赖(monorepo 使用 pnpm workspace);
- 进入套件目录
ts/e2e-tests/runtimes/node/json-schema-to-zod-v3; - 执行
pnpm test:e2e(等价于bun test e2e.test.ts),或先执行pnpm typecheck做静态检查; - 测试将在 Docker 中以 Node.js
22.22.3、24.17.0、25.9.0三个版本分别运行,结果汇总到DEBUG.log。
若想扩展覆盖范围,可以沿用相同的断言模式补充新的 schema 形态(例如oneOf、allOf、patternProperties的往返),或在versions配置中加入新的 Node.js 版本(参考 ts/e2e-tests/_utils/README.md 的版本解析优先级:COMPOSIO_E2E_NODE_VERSION环境变量 >config.versions.node>mise.toml默认值)。需要深入理解转换语义时,建议对照阅读 parse-object.ts 与包内的单元测试(ts/packages/json-schema-to-zod/test),端到端测试与源码实现互为印证。
【免费下载链接】composioComposio powers 1000+ toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action.项目地址: https://gitcode.com/GitHub_Trending/co/composio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考