import { afterEach, expect, test } from "vitest"; import * as z from "zod/v4"; afterEach(() => { z.config({ customError: undefined }); }); /** Error maps run on the first read of `error`, so a test that only observes them reads it. */ const fail = (r: { error?: unknown }) => r.error; /** Collects the owning schema's registered metadata for every issue an error map sees. */ function captureMeta(): { seen: (z.core.GlobalMeta | undefined)[] } { const seen: (z.core.GlobalMeta | undefined)[] = []; z.config({ customError: (issue) => { seen.push(issue.schema && z.globalRegistry.get(issue.schema)); return undefined; }, }); return { seen }; } test("check-originated issues expose the owning schema", () => { const { seen } = captureMeta(); fail(z.object({ name: z.string().min(5).meta({ title: "Name" }) }).safeParse({ name: "ab" })); fail(z.string().max(2).meta({ title: "Max" }).safeParse("abc")); fail(z.email().meta({ title: "Email" }).safeParse("nope")); fail(z.number().multipleOf(3).meta({ title: "Multiple" }).safeParse(4)); fail(z.array(z.string()).min(2).meta({ title: "Array" }).safeParse([])); expect(seen).toEqual([ { title: "Name" }, { title: "Max" }, { title: "Email" }, { title: "Multiple" }, { title: "Array" }, ]); }); test("schema-originated issues expose the owning schema", () => { const { seen } = captureMeta(); fail(z.object({ name: z.string().min(5).meta({ title: "Name" }) }).safeParse({ name: 123 })); fail(z.email().meta({ title: "Email" }).safeParse(123)); expect(seen).toEqual([{ title: "Name" }, { title: "Email" }]); }); test("refinements attribute to the refined schema, not the internal check schema", () => { const { seen } = captureMeta(); fail( z .string() .refine(() => false) .meta({ title: "Refine" }) .safeParse("abc") ); fail( z .string() .superRefine((_, ctx) => ctx.addIssue({ code: "custom", message: "" })) .meta({ title: "SuperRefine" }) .safeParse("abc") ); expect(seen).toEqual([{ title: "Refine" }, { title: "SuperRefine" }]); }); test("the innermost schema owns the issue", () => { const { seen } = captureMeta(); fail( z .array(z.string().min(5).meta({ title: "Element" })) .meta({ title: "Array" }) .safeParse(["ab"]) ); fail( z .string() .pipe(z.string().min(5).meta({ title: "Target" })) .meta({ title: "Pipe" }) .safeParse("ab") ); fail( z .looseObject({}) .check(z.property("a", z.string().meta({ title: "Property" }))) .meta({ title: "Object" }) .safeParse({ a: 1 }) ); expect(seen).toEqual([{ title: "Element" }, { title: "Target" }, { title: "Property" }]); }); test("issues carrying no usable `inst` still get the owning schema", () => { const { seen } = captureMeta(); // A hand-pushed issue may carry no inst at all, and ctx.addIssue(string) puts the check's def — not a Zod object — on inst. fail( z .string() .check((payload) => { payload.issues.push({ code: "custom", input: payload.value }); }) .meta({ title: "Bare" }) .safeParse("abc") ); const shorthand = z .string() .superRefine((_, ctx) => ctx.addIssue("bad stuff")) .meta({ title: "Shorthand" }) .safeParse("abc"); expect(seen).toEqual([{ title: "Bare" }]); expect(shorthand.error!.issues[0].message).toEqual("bad stuff"); }); test("async checks expose the owning schema", async () => { const { seen } = captureMeta(); fail( await z .string() .refine(async () => false) .meta({ title: "Async" }) .safeParseAsync("abc") ); expect(seen).toEqual([{ title: "Async" }]); }); test("the owning schema is the clone `.meta()` registered, not the pre-metadata schema", () => { const bare = z.string().min(5); const labeled = bare.meta({ title: "Labeled" }); const seen: unknown[] = []; z.config({ customError: (issue) => { seen.push(issue.schema === labeled); return undefined; }, }); fail(labeled.safeParse("ab")); expect(seen).toEqual([true]); }); test("`inst` keeps its meaning and `schema` stays off the finalized issue", () => { const insts: string[] = []; z.config({ customError: (issue) => { insts.push(issue.inst!.constructor.name); return undefined; }, }); const tooSmall = z.string().min(5).safeParse("ab").error!; const wrongType = z.string().safeParse(123).error!; expect(insts).toEqual(["$ZodCheckMinLength", "ZodString"]); expect(Object.keys(tooSmall.issues[0])).not.toContain("schema"); expect(Object.keys(wrongType.issues[0])).not.toContain("schema"); }); test("every error map kind receives the owning schema", () => { const seen: Record = {}; const capture = (key: string) => (issue: { schema?: z.core.$ZodType | undefined }) => { seen[key] = issue.schema && z.globalRegistry.get(issue.schema); return undefined; }; fail( z .string() .min(5, { error: capture("check") }) .meta({ title: "Check" }) .safeParse("ab") ); // A schema-bound map is only consulted for issues the schema itself raised, so this one needs a type error. fail( z .string({ error: capture("schema") }) .meta({ title: "Schema" }) .safeParse(123) ); fail( z .string() .min(5) .meta({ title: "Parse" }) .safeParse("ab", { error: capture("parse") }) ); z.config({ customError: capture("customError") }); fail(z.string().min(5).meta({ title: "Custom" }).safeParse("ab")); // localeError carries the default English locale, so it is restored rather than cleared. const localeError = z.config().localeError; z.config({ customError: undefined, localeError: capture("localeError") }); fail(z.string().min(5).meta({ title: "Locale" }).safeParse("ab")); z.config({ localeError }); expect(seen).toEqual({ check: { title: "Check" }, schema: { title: "Schema" }, parse: { title: "Parse" }, customError: { title: "Custom" }, localeError: { title: "Locale" }, }); });