Zod 可选 constructor 字段没传,为什么报 received function
Zod 4.6.7 会沿原型链读取对象字段,缺省的 constructor 可能被当成继承函数。区分自有字段与可选值,在明确的 JSON 对象边界处理输入。
请求没有传 constructor 字段,Zod 却在这个字段上报 Invalid input: expected string, received function。如果 schema 将它声明为可选字符串,先检查输入对象的原型,而不只是请求 JSON 里有没有这个键。
Zod 4.6.7 的对象解析会通过属性读取和 key in input 判断值与存在性,这两种操作都会沿原型链查找。普通对象继承的 constructor、toString 等方法,因此可能被当成请求提供的字段值。这个问题不要求输入遭到原型污染,普通的 JSON.parse('{}') 就有 Object 原型。
可选字段已经读到了一个函数
下面是对应的最小 schema:
import * as z from 'zod';
const schema = z.object({
constructor: z.string().optional(),
});
const result = schema.safeParse({});
optional() 允许缺省或 undefined,不会把一个已经读到的函数忽略。给这个字段加默认字符串,也不能让默认值覆盖原型方法:默认分支处理的并不是“任何不符合类型的值”。
反方向也要留意。把字段改成宽松的 z.unknown(),可能让继承来的函数通过校验,而不是证明请求包含了该字段。不能通过不断放宽 schema 来消除这条报错。
在明确的 JSON 对象边界隔离继承属性
如果 API 的契约就是普通 JSON 对象,可以在进入这一层校验前,将顶层自有字段复制到没有原型的容器中。下面是处理已取得请求文本 rawBody 的示例,不负责网络读取:
const input = JSON.parse(rawBody);
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
throw new TypeError('Expected a JSON object');
}
const ownFields = Object.assign(Object.create(null), input);
const result = schema.safeParse(ownFields);
这样,缺省的 constructor 不再从 Object 原型上取值;请求实际提供了这个自有字段时,仍要按 schema 校验。不要把它用作任意类实例的通用转换:访问器、原型方法和对象身份可能本来就是那类输入的契约。
这只是顶层转换。schema 内若还有使用同类字段名的嵌套对象,需要在对应边界单独设计处理,不能声称一次浅复制已经修复所有层级。复制出来的对象也没有继承 hasOwnProperty() 等方法,后续代码应使用适合无原型对象的访问方式。
如果 API 尚未对外固定字段名,也可以评估换成不与原型成员冲突的名字;已经公开的接口则不能只改服务端 schema,让旧客户端悄悄失效。
分开检查缺省值和真实输入
至少覆盖字段缺省、传入合法字符串、传入错误类型,以及相同字段位于嵌套对象的情况。缺省值应该遵守原来的 optional/default 契约,错误类型仍应被拒绝,输出中不应凭空出现继承函数。
4.6.7 的正式实现仍有上述属性读取规则。切换 JIT、调用 safeParseAsync() 或把对象再做一次 JSON 往返,都不能据此保证绕过原型成员问题;不要修改全局 Object.prototype 来迁就单个接口。


