UNPKG

zod

Version:

TypeScript-first schema declaration and validation library with static type inference

198 lines (161 loc) 6.17 kB
import { expect, test } from "vitest"; import * as z from "zod/v4"; /** * Schema methods are exposed in a way that works when detached from the * schema instance — `const opt = schema.optional; opt()` must produce a * working `ZodOptional`, not a corrupt one. This pattern is used in real * code (e.g. `arr.map(schema.parse)`, `arr.map(schema.optional)`, * destructuring inside utility functions). * * This test caught a regression in colinhacks/zod#5870 where a memory * optimization moved methods to the prototype and made `this`-binding * required, silently breaking any detached usage. */ const probeArgs: Record<string, unknown[]> = { // ZodType optional: [], exactOptional: [], nullable: [], nullish: [], array: [], describe: ["x"], brand: [], readonly: [], default: ["fallback"], catch: ["fallback"], // _ZodString min: [1], max: [10], length: [5], nonempty: [], trim: [], toLowerCase: [], toUpperCase: [], // ZodString format methods email: [], url: [], uuid: [], cuid: [], cuid2: [], ulid: [], base64: [], base64url: [], ipv4: [], ipv6: [], // ZodNumber int: [], positive: [], negative: [], finite: [], }; test("detached parse-family methods work without `this` binding", () => { const schema = z.string(); const { parse, safeParse } = schema; expect(parse("hello")).toBe("hello"); expect(safeParse("hello").success).toBe(true); }); test("detached schema.optional() returns a working ZodOptional", () => { const schema = z.string(); const opt = schema.optional; const detached = opt(); expect(detached).toBeInstanceOf(z.ZodOptional); expect(detached.safeParse("hello").success).toBe(true); expect(detached.safeParse(undefined).success).toBe(true); expect(detached.safeParse(123).success).toBe(false); }); test("detached schema.nullable() returns a working ZodNullable", () => { const schema = z.string(); const nul = schema.nullable; const detached = nul(); expect(detached).toBeInstanceOf(z.ZodNullable); expect(detached.safeParse("hello").success).toBe(true); expect(detached.safeParse(null).success).toBe(true); expect(detached.safeParse(123).success).toBe(false); }); test("detached schema.array() returns a working ZodArray", () => { const schema = z.string(); const arr = schema.array; const detached = arr(); expect(detached).toBeInstanceOf(z.ZodArray); expect(detached.safeParse(["a", "b"]).success).toBe(true); expect(detached.safeParse([1, 2]).success).toBe(false); }); test("detached schema.describe() returns a described schema", () => { const schema = z.string(); const describe = schema.describe; const described = describe("hello world"); expect(described.description).toBe("hello world"); }); test("detached refinement still validates", () => { const schema = z.string(); const refine = schema.refine; const refined = refine((s: string) => s.startsWith("x"), "must start with x"); expect(refined.safeParse("xhello").success).toBe(true); expect(refined.safeParse("hello").success).toBe(false); }); test("detached chained calls work — schema.optional then parse", () => { const schema = z.string(); const opt = schema.optional; const optionalSchema = opt(); const { parse } = optionalSchema; expect(parse("hi")).toBe("hi"); expect(parse(undefined)).toBe(undefined); }); test("detached parse can be called as a free function", () => { const schema = z.string(); const parse = schema.parse; const inputs = ["a", "b", "c"]; const results = inputs.map((v) => parse(v)); expect(results).toEqual(["a", "b", "c"]); }); test("detached methods on z.number() work", () => { const schema = z.number(); const min = schema.min; const max = schema.max; const positive = schema.positive; expect(min(5).safeParse(3).success).toBe(false); expect(max(5).safeParse(7).success).toBe(false); expect(positive().safeParse(-1).success).toBe(false); }); test("detached object methods work", () => { const schema = z.object({ a: z.string(), b: z.number() }); const pick = schema.pick; const omit = schema.omit; const partial = schema.partial; const extend = schema.extend; expect(Object.keys(pick({ a: true })._zod.def.shape)).toEqual(["a"]); expect(Object.keys(omit({ a: true })._zod.def.shape)).toEqual(["b"]); expect(partial().safeParse({}).success).toBe(true); const extended = extend({ c: z.boolean() }); expect(Object.keys(extended._zod.def.shape).sort()).toEqual(["a", "b", "c"]); }); // Sweep across many builder methods at once. If any of them break with the // `const m = schema.foo; m(...)` pattern, this test will report which. test("broad sweep: detaching builder methods does not throw or produce a corrupt schema", () => { const stringSchema = z.string(); const numberSchema = z.number(); const broken: Array<{ method: string; reason: string }> = []; for (const [methodName, args] of Object.entries(probeArgs)) { const target: any = methodName in stringSchema ? stringSchema : methodName in numberSchema ? numberSchema : null; if (!target) continue; const detached = target[methodName] as Function | undefined; if (typeof detached !== "function") continue; try { const result = detached(...args); // If the detached call returned a schema, sanity-check it parses // its base type. (e.g. `optional()` should accept its inner type.) if (result && typeof result === "object" && "_zod" in result && typeof (result as any).safeParse === "function") { const probeValue = target === stringSchema ? "x" : 1; const r = (result as any).safeParse(probeValue); // success or a clean failure are both fine — we only fail on throw or // on a schema with corrupt internal state (innerType undefined etc). if (r === undefined || (typeof r === "object" && !("success" in r))) { broken.push({ method: methodName, reason: "safeParse returned malformed result" }); } } } catch (err: any) { broken.push({ method: methodName, reason: err?.message ?? String(err) }); } } expect(broken).toEqual([]); });