808+
809+const prototypePropertyNames = ["__proto__", "constructor", "toString", "hasOwnProperty", "valueOf"] as const;
810+
811+test.each(prototypePropertyNames)("z.formatError handles Object.prototype property path: %s", (name) => {
812+ const schema = z.object({ data: z.string() }).superRefine((_, ctx) => {
813+ ctx.addIssue({ code: "custom", message: "invalid value", path: [name] });
814+ });
815+ const result = schema.safeParse({ data: "hello" });
816+ expect(result.success).toBe(false);
817+ const formatted = z.formatError(result.error!);
818+ expect(Object.prototype.hasOwnProperty.call(formatted, name)).toBe(true);
819+ expect(Object.getPrototypeOf(formatted)).toBe(Object.prototype);
820+ expect((formatted as any)[name]._errors).toEqual(["invalid value"]);
821+});
822+
823+test.each(prototypePropertyNames)("z.treeifyError handles Object.prototype property path: %s", (name) => {
824+ const schema = z.object({ data: z.string() }).superRefine((_, ctx) => {
825+ ctx.addIssue({ code: "custom", message: "invalid value", path: [name] });
826+ });
827+ const result = schema.safeParse({ data: "hello" });
828+ expect(result.success).toBe(false);
829+ const tree = z.treeifyError(result.error!);
830+ expect(Object.prototype.hasOwnProperty.call(tree.properties, name)).toBe(true);
831+ expect(Object.getPrototypeOf(tree.properties)).toBe(Object.prototype);
832+ expect((tree as any).properties[name].errors).toEqual(["invalid value"]);
833+});
834+
835+test.each(prototypePropertyNames)("z.flattenError handles Object.prototype property path: %s", (name) => {
836+ const schema = z.object({ data: z.string() }).superRefine((_, ctx) => {
837+ ctx.addIssue({ code: "custom", message: "invalid value", path: [name] });
838+ });
839+ const result = schema.safeParse({ data: "hello" });
840+ expect(result.success).toBe(false);
841+ const { fieldErrors } = z.flattenError(result.error!);
842+ expect(Object.prototype.hasOwnProperty.call(fieldErrors, name)).toBe(true);
843+ expect(Object.getPrototypeOf(fieldErrors)).toBe(Object.prototype);
844+ expect((fieldErrors as any)[name]).toEqual(["invalid value"]);
845+});
846+
847+/** The "__proto__" node must be a real own data property, not the inherited accessor. */
848+const protoNode = (obj: any) => Object.getOwnPropertyDescriptor(obj, "__proto__")!.value;
849+
850+test("error formatters merge sibling issues under a __proto__ path", () => {
851+ const schema = z.string().check((ctx) => {
852+ ctx.issues.push({ code: "custom", message: "a", path: ["__proto__", "x"], input: ctx.value });
853+ ctx.issues.push({ code: "custom", message: "b", path: ["__proto__", "y"], input: ctx.value });
854+ });
855+ const error = schema.safeParse("hello").error!;
856+
857+ const formatted = protoNode(z.formatError(error));
858+ expect(formatted.x._errors).toEqual(["a"]);
859+ expect(formatted.y._errors).toEqual(["b"]);
860+
861+ const tree = protoNode((z.treeifyError(error) as any).properties);
862+ expect(tree.properties.x.errors).toEqual(["a"]);
863+ expect(tree.properties.y.errors).toEqual(["b"]);
864+
865+ expect(({} as any).x).toBeUndefined();
866+ expect(({} as any).y).toBeUndefined();
867+});
868+
869+test("error formatting leaves Object.prototype untouched for input-derived keys", () => {
870+ const record = z.record(z.string(), z.string()).safeParse(JSON.parse('{"toString": 1}'));
871+ expect(record.success).toBe(false);
872+ expect((z.formatError(record.error!) as any).toString._errors).toHaveLength(1);
873+
874+ const entries = new Map(Object.entries(JSON.parse('{"__proto__": {"pwn": 1}}')));
875+ const result = z.map(z.string(), z.object({ pwn: z.string() })).safeParse(entries);
876+ expect(result.success).toBe(false);
877+ expect(protoNode(z.formatError(result.error!)).pwn._errors).toHaveLength(1);
878+ expect(protoNode((z.treeifyError(result.error!) as any).properties).properties.pwn.errors).toHaveLength(1);
879+ expect(({} as any).pwn).toBeUndefined();
880+});