@composio/json-schema-to-zod 的 Zod v3 兼容性端到端测试指南
2026/9/12 18:20:59 网站建设 项目流程

@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.324.17.025.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:e2etest: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(解析为ZodNeverpropertiesSchema.strict()
有命名属性 +additionalProperties: true(显式)propertiesSchema.passthrough()
有命名属性 +additionalProperties为 schemapropertiesSchema.catchall(schema)
有命名属性 + 省略additionalPropertiespropertiesSchema.strict()(对工具输入保持严格)
无命名属性 +additionalProperties: falsez.object({}).strict()
无命名属性 +additionalProperties: true(显式)z.object({}).passthrough()
无命名属性 +additionalProperties为 schemaz.record(schema)
无命名属性 + 省略additionalPropertiesz.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.10.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 版本的机器验证。

本地复现步骤:

  1. 在仓库根目录安装依赖(monorepo 使用 pnpm workspace);
  2. 进入套件目录ts/e2e-tests/runtimes/node/json-schema-to-zod-v3
  3. 执行pnpm test:e2e(等价于bun test e2e.test.ts),或先执行pnpm typecheck做静态检查;
  4. 测试将在 Docker 中以 Node.js22.22.324.17.025.9.0三个版本分别运行,结果汇总到DEBUG.log

若想扩展覆盖范围,可以沿用相同的断言模式补充新的 schema 形态(例如oneOfallOfpatternProperties的往返),或在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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询