Zod 校验已经失败,整理错误时又报读取 push 失败
Zod 4.4.3 的 formatError 和 treeifyError 遇到 toString 等路径可能再次抛错。区分校验结果与错误树构造,核对正式修正并保留动态键。
safeParse() 已经正常返回失败结果,接着调用 z.treeifyError() 或 z.formatError(),却又抛出:
TypeError: Cannot read properties of undefined (reading 'push')
如果错误路径中包含 toString、constructor 等与 Object 原型成员同名的键,检查是否使用 Zod 4.4.3 的错误格式化实现。这次异常发生在整理校验错误时,不表示最初的非法输入通过了校验。
错误树误读了继承属性
formatError 与 treeifyError 会把 issues 中的路径逐级放进对象。旧实现用真值或空值判断来决定某个节点是否存在,但普通对象会继承 toString、constructor 等属性。
读取这些名字时,代码可能得到继承的函数,误以为已有错误节点,随后访问并不存在的 errors 或 _errors 数组,最终在 push 处抛错。__proto__ 还涉及继承的 setter,不能只用一次普通赋值就认定创建了自有键。
动态字段、record 校验和自定义 addIssue 路径,都可能带来这样的名字。下面是将错误放到固定 toString 路径的示例:
import * as z from 'zod';
const schema = z.object({ data: z.string() }).superRefine((value, ctx) => {
ctx.addIssue({
code: 'custom',
path: ['toString'],
message: '该字段的业务规则未满足',
});
});
const result = schema.safeParse({ data: 'hello' });
if (!result.success) {
const tree = z.treeifyError(result.error);
console.log(tree);
}
这个自定义规则故意产生一条错误,只为表达触发条件;并没有声称字符串 hello 本身不符合 z.string。
使用包含自有属性保护的正式版本
正式 Zod 4.6.7 的对应实现,会检查节点是否为自有属性,并用 defineProperty 建立相关节点,避免把继承成员当成错误容器。4.4.3 命中这一故障的项目,可以沿现有升级流程采用含该处理的正式版本,再核对实际加载的是哪份 Zod。
这里能确认 4.6.7 包含修正,不据此指定最早修复版本,也不把尚未进入所用发布物的补丁当成现成修复。切换依赖后,错误展示层若仍使用另一份旧副本,也需要一起对照。
暂时无法升级时,错误响应可以先按现有协议从 result.error.issues 读取路径与消息,避免再次调用有问题的树构造器。但这不是直接把完整 issue 对象对外输出的理由;服务端错误响应仍应只暴露协议允许的字段。
不要为隐藏格式化异常,删除所有与原型成员同名的错误路径。这会让用户看不到真正的问题字段。也不要修改全局 Object.prototype,或在第一次格式化失败后把校验结果改成成功。
与可选输入字段报错分开检查
同名键也可能在更早的对象解析阶段出现问题,例如没有传 constructor 字段,却被读取到继承函数。那是输入字段访问路径;此处是校验已失败后构造输出错误树。两处需要分别核对,修复格式化器不代表所有输入访问规则都改变了。
验收应覆盖普通字段、嵌套字段、toString 和 constructor 路径,并确认非法输入仍然被拒绝、错误消息归到正确节点。项目若允许任意动态键,还应检查这些键展示时的转义与响应结构,不能只确认 push 异常消失。


