808
809const prototypePropertyNames = ["__proto__", "constructor", "toString", "hasOwnProperty", "valueOf"] as const;
810
811test.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
823test.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
835test.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. */
848const protoNode = (obj: any) => Object.getOwnPropertyDescriptor(obj, "__proto__")!.value;
849
850test("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
869test("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});