zod
Version:
TypeScript-first schema declaration and validation library with static type inference
236 lines (212 loc) • 10.9 kB
text/typescript
import { expect, expectTypeOf, test } from "vitest";
import { z } from "zod/v4";
/* ------------------------------------------------------------------ *
* TYPE-LEVEL — the dangerous half. Defaults must stay OPTIONAL in
* z.input and REQUIRED in z.output, exactly as before the ladder.
* ------------------------------------------------------------------ */
test("ladder: z.input / z.output unchanged by the extra rung", () => {
const objDefault = z.object({ a: z.string().default("D"), b: z.number() });
expectTypeOf<z.input<typeof objDefault>>().toEqualTypeOf<{ a?: string | undefined; b: number }>();
expectTypeOf<z.output<typeof objDefault>>().toEqualTypeOf<{ a: string; b: number }>();
const objPrefault = z.object({ a: z.string().prefault("P"), b: z.number() });
expectTypeOf<z.input<typeof objPrefault>>().toEqualTypeOf<{ a?: string | undefined; b: number }>();
expectTypeOf<z.output<typeof objPrefault>>().toEqualTypeOf<{ a: string; b: number }>();
const objOptional = z.object({ a: z.string().optional() });
expectTypeOf<z.input<typeof objOptional>>().toEqualTypeOf<{ a?: string | undefined }>();
const objTransform = z.object({
a: z
.string()
.default("")
.transform((v) => v.length),
});
expectTypeOf<z.input<typeof objTransform>>().toEqualTypeOf<{ a?: string | undefined }>();
expectTypeOf<z.output<typeof objTransform>>().toEqualTypeOf<{ a: number }>();
const standalone = z.string().default("D");
expectTypeOf<z.input<typeof standalone>>().toEqualTypeOf<string | undefined>();
expectTypeOf<z.output<typeof standalone>>().toEqualTypeOf<string>();
// a trailing defaulted tuple slot stays optional in the input tuple
const tup = z.tuple([z.string(), z.number().default(1)]);
expectTypeOf<z.input<typeof tup>>().toEqualTypeOf<[string, (number | undefined)?]>();
const partialed = objDefault.partial();
expectTypeOf<z.input<typeof partialed>>().toEqualTypeOf<{ a?: string | undefined; b?: number | undefined }>();
});
/* ------------------------------------------------------------------ *
* RUNTIME
* ------------------------------------------------------------------ */
test("ladder: the #6321 regression is fixed", () => {
const arr = z
.string()
.default("")
.transform((v) => (v ? v.split(",") : []));
expect(z.object({ a: arr }).partial().parse({})).toEqual({ a: [] });
expect(z.object({ a: arr }).partial().parse({ a: undefined })).toEqual({ a: [] });
expect(z.object({ a: arr }).partial().parse({ a: "x,y" })).toEqual({ a: ["x", "y"] });
expect(arr.optional().parse(undefined)).toEqual([]);
expect(
z
.string()
.prefault("hi")
.transform((v) => v.length)
.optional()
.parse(undefined)
).toBe(2);
});
test("ladder: key-level admissibility across every rung", () => {
expect(z.object({ a: z.string().default("D") }).parse({})).toEqual({ a: "D" });
expect(z.object({ a: z.string().prefault("P") }).parse({})).toEqual({ a: "P" });
expect(z.object({ a: z.string().optional() }).parse({})).toEqual({});
expect(z.object({ a: z.transform(() => "T") }).parse({})).toEqual({ a: "T" });
expect(z.object({ a: z.string().catch("C") }).parse({})).toEqual({ a: "C" });
expect(z.object({ a: z.string() }).safeParse({}).success).toBe(false);
});
test("ladder: exactOptional keeps its distinct meaning", () => {
expect(z.object({ a: z.string().exactOptional() }).parse({})).toEqual({});
expect(z.object({ a: z.string().exactOptional() }).safeParse({ a: undefined }).success).toBe(false);
});
// An absent key never materializes a value from a schema that only claims the middle rung, however that schema answers `undefined`. The top rung still substitutes, and an explicitly present `undefined` still runs the inner.
test.each([true, false])("ladder: an absent key stays absent on the middle rung (jitless=%s)", (jitless) => {
const opts = { jitless };
expect(z.object({ a: z.coerce.string().exactOptional() }).parse({}, opts)).toStrictEqual({});
expect(z.object({ a: z.string().catch("C").exactOptional() }).parse({}, opts)).toStrictEqual({});
expect(z.object({ a: z.preprocess((v) => v ?? "X", z.string()).exactOptional() }).parse({}, opts)).toStrictEqual({});
expect(z.object({ a: z.union([z.coerce.string(), z.string().optional()]) }).parse({}, opts)).toStrictEqual({});
expect(z.object({ a: z.string().default("D").exactOptional() }).parse({}, opts)).toStrictEqual({ a: "D" });
expect(z.object({ a: z.string().prefault("P").exactOptional() }).parse({}, opts)).toStrictEqual({ a: "P" });
expect(z.object({ a: z.coerce.string().exactOptional() }).parse({ a: undefined }, opts)).toStrictEqual({
a: "undefined",
});
expect(z.object({ a: z.coerce.string() }).parse({ a: undefined }, opts)).toStrictEqual({ a: "undefined" });
});
test("ladder: an absent tuple slot on the middle rung truncates the tail", () => {
expect(z.tuple([z.string(), z.coerce.string().exactOptional()]).parse(["x"])).toStrictEqual(["x"]);
expect(z.tuple([z.string(), z.string().catch("C").exactOptional()]).parse(["x"])).toStrictEqual(["x"]);
expect(z.tuple([z.string(), z.string().default("D").exactOptional()]).parse(["x"])).toStrictEqual(["x", "D"]);
});
// z.compile() assembles its own output, so the gate has to be mirrored there or compile mode silently keeps the invented value.
test("ladder: compile mode agrees with the interpreted and JIT paths on absent middle-rung slots", () => {
const objects = [
[z.object({ a: z.coerce.string().exactOptional() }), {}],
[z.object({ a: z.string().catch("C").exactOptional() }), {}],
[z.object({ a: z.string().default("D").exactOptional() }), { a: "D" }],
[z.object({ a: z.string().catch("C") }), { a: "C" }],
] as const;
for (const [schema, expected] of objects) {
expect(z.compile(schema).parse({})).toStrictEqual(expected);
expect(schema.parse({}, { jitless: true })).toStrictEqual(expected);
expect(schema.parse({})).toStrictEqual(expected);
}
const tuples = [
[z.tuple([z.string(), z.coerce.string().exactOptional()]), ["x"]],
[z.tuple([z.string(), z.string().default("D").exactOptional()]), ["x", "D"]],
] as const;
for (const [schema, expected] of tuples) {
expect(z.compile(schema).parse(["x"])).toStrictEqual(expected);
expect(schema.parse(["x"], { jitless: true })).toStrictEqual(expected);
}
});
test("ladder: tuple minimum length respects every rung", () => {
expect(z.tuple([z.string().default("D")]).parse([])).toEqual(["D"]);
expect(z.tuple([z.string().prefault("P")]).parse([])).toEqual(["P"]);
expect(z.tuple([z.string().optional()]).parse([])).toEqual([]);
expect(z.tuple([z.string()]).safeParse([]).success).toBe(false);
expect(z.tuple([z.string(), z.number().default(1)]).parse(["x"])).toEqual(["x", 1]);
});
test("ladder: the #5941 / #5939 clobber invariants still hold", () => {
expect(
z
.preprocess((v) => v ?? "X", z.string())
.optional()
.parse(undefined)
).toBeUndefined();
expect(z.string().catch("X").optional().parse(undefined)).toBeUndefined();
expect(
z
.string()
.catch("X")
.transform((s) => `${s}!`)
.optional()
.parse(undefined)
).toBeUndefined();
expect(
z
.transform(() => "T")
.optional()
.parse(undefined)
).toBeUndefined();
expect(
z
.union([z.preprocess((v) => v ?? "X", z.string()), z.number()])
.optional()
.parse(undefined)
).toBeUndefined();
expect(z.object({ a: z.preprocess((v) => v ?? "X", z.string()) }).parse({})).toEqual({ a: "X" });
});
test("ladder: the top rung survives every wrapper order", () => {
expect(z.string().default("D").optional().parse(undefined)).toBe("D");
expect(z.string().default("D").optional().optional().parse(undefined)).toBe("D");
expect(z.string().default("D").nullable().optional().parse(undefined)).toBe("D");
expect(z.string().default("D").readonly().optional().parse(undefined)).toBe("D");
expect(z.string().default("D").catch("C").optional().parse(undefined)).toBe("D");
expect(z.string().default("D").pipe(z.string()).optional().parse(undefined)).toBe("D");
expect(
z
.union([z.string().default("D"), z.number()])
.optional()
.parse(undefined)
).toBe("D");
expect(
z
.lazy(() => z.string().default("D"))
.optional()
.parse(undefined)
).toBe("D");
});
test("ladder: record and catchall paths that read optin", () => {
expect(z.record(z.string(), z.string().default("D")).parse({ k: undefined })).toEqual({ k: "D" });
expect(z.object({}).catchall(z.string().default("D")).parse({ x: undefined })).toEqual({ x: "D" });
});
test("ladder: async parity", async () => {
const a = z
.string()
.default("")
.transform(async (v) => (v ? v.split(",") : []));
expect((await a.optional().safeParseAsync(undefined)).data).toEqual([]);
expect((await z.object({ a }).partial().safeParseAsync({})).data).toEqual({ a: [] });
expect(
(
await z
.preprocess(async (v) => v ?? "X", z.string())
.optional()
.safeParseAsync(undefined)
).data
).toBeUndefined();
});
test("ladder: the rung values themselves", () => {
expect(z.string()._zod.optin).toBeUndefined();
expect(z.string().optional()._zod.optin).toBe("optional");
expect(z.transform(() => 1)._zod.optin).toBe("optional");
expect(z.string().catch("C")._zod.optin).toBe("optional");
expect(z.string().default("D")._zod.optin).toBe("defaulted");
expect(z.string().prefault("P")._zod.optin).toBe("defaulted");
expect(
z
.string()
.default("D")
.transform((v) => v)._zod.optin
).toBe("defaulted");
expect(z.string().default("D").optional()._zod.optin).toBe("defaulted");
expect(z.union([z.string().default("D"), z.number()])._zod.optin).toBe("defaulted");
expect(z.union([z.string().optional(), z.number()])._zod.optin).toBe("optional");
});
test("exactOptional overrides the values and pattern optional installs", () => {
expect(z.exactOptional(z.enum(["a", "b"]))._zod.values).toEqual(new Set(["a", "b"]));
// a format carries its pattern on its def; a chained `.regex()` no longer folds into the inner `_zod.pattern`, the template literal derives that itself
expect(z.exactOptional(z.stringFormat("abc", /^abc$/))._zod.pattern).toEqual(/^abc$/);
expect(z.templateLiteral(["a", z.exactOptional(z.literal("b"))]).safeParse("a").success).toEqual(false);
});
// The override is installed once, on a prototype every instance of the type shares, so it has to hold for instances built after the first.
test("exactOptional's override holds for later instances", () => {
z.exactOptional(z.enum(["a", "b"]));
expect(z.exactOptional(z.enum(["c", "d"]))._zod.values).toEqual(new Set(["c", "d"]));
expect(z.exactOptional(z.stringFormat("xyz", /^xyz$/))._zod.pattern).toEqual(/^xyz$/);
});