OpenCode 学习笔记:Zod Schema 与类型定义
2026/4/27大约 2 分钟
OpenCode 学习笔记:Zod Schema 与类型定义
1. 关于 Zod Schema 和 .optional() 的理解
问题:
在 prompt.ts 中有如下代码:
format: MessageV2.Format.optional(), // 期望的输出格式(如 JSON Schema)
system: z.string().optional(),对于以上代码,能理解 system 要返回 string 类型,但是前面那个不理解。
解答:MessageV2.Format.optional() 的原理和 z.string().optional() 是一模一样的,因为 MessageV2.Format 本质上也是一个 Zod Schema 对象,而不是普通的 TypeScript 类型(interface 或 type)。
在 src/session/message-v2.ts 文件中,它是这样定义的:
export const OutputFormatText = z.object({
type: z.literal("text"),
});
export const OutputFormatJsonSchema = z.object({
type: z.literal("json_schema"),
schema: z.record(z.string(), z.any()),
retryCount: z.number().int().min(0).default(2),
});
// 👇 这里定义了 Format,它是一个 Zod 的 discriminatedUnion(可辨识联合类型)
export const Format = z.discriminatedUnion("type", [OutputFormatText, OutputFormatJsonSchema]);为什么可以调用 .optional()?
Format是一个 Zod 实例:因为Format是通过z.discriminatedUnion(...)创建出来的,所以它本身就是一个 Zod Schema 对象。- 继承了 Zod 的方法:所有的 Zod Schema 对象(不论是
z.string()还是复杂的z.object()或z.union())都会具备一系列通用的方法,其中就包括.optional()。 - 作用:当你在后面加上
.optional()后,它就表示format这个字段在输入时可以不传(是undefined),但如果传了,就必须符合Format这个 Zod Schema 定义的规则。
2. Zod Schema 的命名拆分与组织
问题:
这个 Zod Schema 对象为什么不叫 Format 而是叫 OutputFormatText,让人理解出现困扰。
解答:
其实真正被外部调用的那个联合 Zod Schema 对象,真的叫做 Format。在 prompt.ts 里使用的正是 MessageV2.Format.optional()。
代码中是这样拆分组织的:
// 1. 定义第一种输出格式:纯文本(零件 1)
export const OutputFormatText = z.object({
type: z.literal("text"),
})
// 2. 定义第二种输出格式:JSON Schema(零件 2)
export const OutputFormatJsonSchema = z.object({
type: z.literal("json_schema"),
schema: z.record(z.string(), z.any()),
retryCount: z.number().int().min(0).default(2),
})
// 3. 将前面两种格式合并成一个最终的 Format 对象!(成品)
export const Format = z.discriminatedUnion("type", [OutputFormatText, OutputFormatJsonSchema])为什么要拆出 OutputFormatText 这种名字?
想象一下,如果不拆开命名,而是直接定义一个巨大的 Format:
export const Format = z.discriminatedUnion("type", [
z.object({ type: z.literal("text") }),
z.object({
type: z.literal("json_schema"),
schema: z.record(z.string(), z.any()),
retryCount: z.number().int().min(0).default(2),
})
])如果类型分支少看起来还好,但在大型项目中拆开有以下好处:
- 防嵌套过深:联合类型(Union)里如果有 5 个、10 个不同的对象,全部堆在一个
Format里面会导致代码嵌套极深,可读性极差。 - 单独复用:系统内部可能某些方法明确只接受纯文本格式,这时候可以直接使用
MessageV2.OutputFormatText这个严格的对象进行校验,而不需要去使用包含 JSON Schema 验证的Format大对象。
总结:
你看到的是它的组装零件(OutputFormatText 等),但在业务代码中用的是组装好的成品(Format)。它实际上是在用乐高积木拼装:
- 零件 1:
OutputFormatText - 零件 2:
OutputFormatJsonSchema - 成品:
Format= 零件 1 + 零件 2