import { describe, expect, expectTypeOf, test } from "vitest"; import * as z from "zod"; import { visit } from "../../core/visit.js"; import { deepPartial } from "../deep-partial.js"; describe("deepPartial", () => { test("makes shallow object optional", () => { const schema = z.object({ a: z.string(), b: z.number() }); const partial = deepPartial(schema); expect(partial.parse({})).toEqual({}); expect(partial.parse({ a: "x" })).toEqual({ a: "x" }); expect(partial.parse({ a: "x", b: 1 })).toEqual({ a: "x", b: 1 }); }); test("makes nested object optional at every level", () => { const schema = z.object({ name: z.string(), profile: z.object({ bio: z.string(), age: z.number(), }), }); const partial = deepPartial(schema); expect(partial.parse({})).toEqual({}); expect(partial.parse({ profile: {} })).toEqual({ profile: {} }); expect(partial.parse({ profile: { bio: "x" } })).toEqual({ profile: { bio: "x" } }); }); test("recurses through arrays", () => { const schema = z.object({ items: z.array(z.object({ a: z.string(), b: z.number() })) }); const partial = deepPartial(schema); expect(partial.parse({ items: [{}, { a: "x" }] })).toEqual({ items: [{}, { a: "x" }] }); }); test("recurses through tuples and rest", () => { const schema = z.tuple([z.object({ a: z.string() }), z.object({ b: z.number() })], z.object({ c: z.boolean() })); const partial = deepPartial(schema); expect(partial.parse([{}, {}])).toEqual([{}, {}]); expect(partial.parse([{}, {}, {}])).toEqual([{}, {}, {}]); }); test("recurses through unions", () => { const schema = z.union([z.object({ a: z.string() }), z.object({ b: z.number() })]); const partial = deepPartial(schema); expect(partial.parse({})).toEqual({}); }); test("recurses through optional/nullable wrappers", () => { const schema = z.object({ x: z.object({ a: z.string() }).optional() }); const partial = deepPartial(schema); expect(partial.parse({})).toEqual({}); expect(partial.parse({ x: {} })).toEqual({ x: {} }); }); test("terminates on recursive (lazy) schemas", () => { const Node: z.ZodType = z.object({ name: z.string(), children: z.array(z.lazy(() => Node)).optional(), }); const partial = deepPartial(Node); expect(partial.parse({})).toEqual({}); expect(partial.parse({ name: "root", children: [{ name: "child" }] })).toEqual({ name: "root", children: [{ name: "child" }], }); expect(partial.parse({ children: [{}] })).toEqual({ children: [{}] }); }); test("preserves primitives at leaves", () => { const schema = z.object({ a: z.string().min(1), b: z.number().int() }); const partial = deepPartial(schema); expect(() => partial.parse({ a: "" })).toThrow(); expect(() => partial.parse({ b: 1.5 })).toThrow(); expect(partial.parse({ a: "ok", b: 1 })).toEqual({ a: "ok", b: 1 }); }); test("keyof preserved on output type", () => { const schema = z.object({ name: z.string(), profile: z.object({ bio: z.string() }), }); type Out = z.output extends infer X ? X : never; const partial = deepPartial(schema); type PartialOut = z.output; expectTypeOf().toEqualTypeOf(); }); test("preserves ZodObject structural type; `.shape` is accessible", () => { const schema = z.object({ name: z.string(), nested: z.object({ age: z.number() }), }); const partial = deepPartial(schema); expectTypeOf(partial).toExtend(); // Each shape entry is a ZodOptional wrapping the deep-partialed inner. expectTypeOf(partial.shape.name).toExtend>(); expectTypeOf(partial.shape.nested).toExtend>(); // Nested shape access is still typed. const innerShape = partial.shape.nested._zod.def.innerType.shape; expectTypeOf(innerShape.age).toExtend>(); }); test("recursive self-reference (via getter) is typeable (no TS depth explosion)", () => { const Category = z.object({ name: z.string(), get subcategories() { return z.array(Category).optional(); }, }); const partial = deepPartial(Category); expectTypeOf(partial).toExtend(); // Should not blow up TS recursion depth. We don't assert on the inner recursive reference — depth-limited DeepPartial shells out after one unfold. expect(partial.parse({ subcategories: [{}] })).toEqual({ subcategories: [{}] }); }); test("preserves ZodArray / ZodTuple / ZodUnion wrappers", () => { const arr = deepPartial(z.array(z.object({ a: z.string() }))); expectTypeOf(arr).toExtend(); const tup = deepPartial(z.tuple([z.object({ a: z.string() }), z.object({ b: z.number() })])); expectTypeOf(tup).toExtend(); const uni = deepPartial(z.union([z.object({ a: z.string() }), z.object({ b: z.number() })])); expectTypeOf(uni).toExtend(); }); test("3-level deep nesting", () => { const schema = z.object({ a: z.object({ b: z.object({ c: z.object({ d: z.string() }), }), }), }); const partial = deepPartial(schema); expect(partial.parse({})).toEqual({}); expect(partial.parse({ a: {} })).toEqual({ a: {} }); expect(partial.parse({ a: { b: { c: {} } } })).toEqual({ a: { b: { c: {} } } }); }); test("discriminated union: degrades to plain union (discriminator becomes optional)", () => { const schema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("a"), a: z.string() }), z.object({ kind: z.literal("b"), b: z.number() }), ]); const partial = deepPartial(schema); expect(partial.parse({ kind: "a", a: "x" })).toEqual({ kind: "a", a: "x" }); expect(partial.parse({ kind: "b", b: 1 })).toEqual({ kind: "b", b: 1 }); expect(partial.parse({})).toEqual({}); }); test("intersection", () => { const schema = z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() })); const partial = deepPartial(schema); expect(partial.parse({})).toEqual({}); expect(partial.parse({ a: "x" })).toEqual({ a: "x" }); expect(partial.parse({ b: 1 })).toEqual({ b: 1 }); }); test("record / map / set value schemas are recursed", () => { const rec = deepPartial(z.record(z.string(), z.object({ a: z.string() }))); expect(rec.parse({ k: {} })).toEqual({ k: {} }); const m = deepPartial(z.map(z.string(), z.object({ a: z.string() }))); expect(m.parse(new Map([["k", {}]]))).toEqual(new Map([["k", {}]])); const s = deepPartial(z.set(z.object({ a: z.string() }))); expect(s.parse(new Set([{}]))).toEqual(new Set([{}])); }); test("wrapper composition (optional + default + readonly)", () => { const schema = z.object({ x: z.object({ a: z.string() }).default({ a: "hi" }).readonly(), }); const partial = deepPartial(schema); expect(partial.parse({})).toEqual({ x: { a: "hi" } }); expect(partial.parse({ x: {} })).toEqual({ x: {} }); }); test("transform standalone is left untouched", () => { const schema = z.object({ a: z.string().transform((s) => s.length) }); const partial = deepPartial(schema); expect(partial.parse({ a: "hi" })).toEqual({ a: 2 }); expect(partial.parse({})).toEqual({}); }); test("pipe input and output are both recursed", () => { const schema = z.object({ a: z.string() }).pipe(z.object({ a: z.string() })); const partial = deepPartial(schema); expect(partial.parse({})).toEqual({}); }); test("tuple elements stay required — only *objects inside* become partial", () => { const schema = z.tuple([z.object({ a: z.string() }), z.string()]); const partial = deepPartial(schema); // Positional items remain required: the second item (z.string) must be present. expect(() => partial.parse([{}])).toThrow(); expect(partial.parse([{}, "x"])).toEqual([{}, "x"]); // The object INSIDE the tuple has its fields made optional. expect(partial.parse([{ a: "y" }, "x"])).toEqual([{ a: "y" }, "x"]); }); test("intersection: both sides are deep-partialed independently", () => { const schema = z.intersection( z.object({ a: z.object({ inner: z.string() }) }), z.object({ b: z.object({ inner: z.number() }) }) ); const partial = deepPartial(schema); expectTypeOf(partial).toExtend(); // Inner objects on either side accept their field as optional. expect(partial.parse({ a: {}, b: {} })).toEqual({ a: {}, b: {} }); // Top-level props also optional (deepPartial partials both object sides). expect(partial.parse({})).toEqual({}); }); test("ZodPromise: inner is recursed", () => { const schema = z.promise(z.object({ a: z.string() })); const partial = deepPartial(schema); expectTypeOf(partial).toExtend(); }); test("ZodSuccess: inner is recursed", () => { const schema = z.success(z.object({ a: z.string() })); const partial = deepPartial(schema); expectTypeOf(partial).toExtend(); }); test("ZodCodec: both sides recursed; preserved as ZodCodec (not degraded to ZodPipe)", () => { const schema = z.codec(z.object({ a: z.string() }), z.object({ a: z.string() }), { decode: (v) => v, encode: (v) => v, }); const partial = deepPartial(schema); expectTypeOf(partial).toExtend(); expect(partial.parse({})).toEqual({}); }); test("ZodPreprocess: inner is recursed; preserved as ZodPreprocess (not degraded to ZodPipe)", () => { const schema = z.preprocess((v) => v, z.object({ a: z.string() })); const partial = deepPartial(schema); expectTypeOf(partial).toExtend(); expect(partial.parse({})).toEqual({}); }); test("catchall value schema is recursed; strict/loose catchalls are preserved", () => { const partial = deepPartial(z.object({ a: z.string() }).catchall(z.object({ n: z.number() }))); expect(partial.parse({ extra: {} })).toEqual({ extra: {} }); expect(() => partial.parse({ extra: { n: "no" } })).toThrow(); // z.strictObject / z.looseObject encode their behavior as a catchall too. expect(() => deepPartial(z.strictObject({ a: z.object({ b: z.string() }) })).parse({ zzz: 1 })).toThrow(); expect(deepPartial(z.looseObject({ a: z.object({ b: z.string() }) })).parse({ a: {}, zzz: 1 })).toEqual({ a: {}, zzz: 1, }); }); test("discriminated union nested inside an array degrades and still matches", () => { const schema = z.object({ shapes: z.array( z.discriminatedUnion("kind", [ z.object({ kind: z.literal("circle"), meta: z.object({ id: z.string() }) }), z.object({ kind: z.literal("square"), side: z.number() }), ]) ), }); const partial = deepPartial(schema); const element = (partial.shape.shapes._zod.def.innerType as z.ZodArray)._zod.def.element; expect(element._zod.def.type).toEqual("union"); expect((element._zod.def as { discriminator?: string }).discriminator).toBeUndefined(); expect(partial.parse({ shapes: [{ kind: "circle", meta: {} }, {}] })).toEqual({ shapes: [{ kind: "circle", meta: {} }, {}], }); }); test("does not mutate the source schema", () => { const inner = z.object({ b: z.string() }); const schema = z.object({ a: inner }); deepPartial(schema); expect(schema.shape.a).toBe(inner); expect(schema.safeParse({}).success).toEqual(false); expect(inner.safeParse({}).success).toEqual(false); }); }); describe("visit (internal)", () => { test("identity callback returns the input schema unchanged", () => { const schema = z.object({ a: z.string(), nested: z.object({ b: z.number() }) }); expect(visit(schema, (s) => s)).toBe(schema); }); test("empty handler map returns the input schema unchanged", () => { const schema = z.object({ a: z.string(), nested: z.object({ b: z.number() }) }); expect(visit(schema, {})).toBe(schema); }); test("only nodes touched by a handler are re-cloned; siblings keep identity", () => { const a = z.string(); const b = z.number(); const schema = z.object({ a, b }); const result = visit(schema, { number: () => z.string() }) as z.ZodType; // `a` was not rewritten; its reference flows through. expect((result._zod.def as any).shape.a).toBe(a); // `b` was rewritten; parent is a new clone. expect(result).not.toBe(schema); }); test("callback: can target a specific def.type", () => { const schema = z.object({ a: z.string(), b: z.number() }); const allStrings = visit(schema, (s) => (s._zod.def.type === "number" ? z.string() : s)) as z.ZodType; expect(allStrings.parse({ a: "x", b: "y" } as any)).toEqual({ a: "x", b: "y" }); }); test("handler map: dispatches by kind; unhandled kinds pass through", () => { const schema = z.object({ a: z.string(), b: z.number() }); const result = visit(schema, { number: () => z.string() }) as z.ZodType; expect(result.parse({ a: "x", b: "y" } as any)).toEqual({ a: "x", b: "y" }); }); test("visits shared sub-schemas only once", () => { const inner = z.object({ a: z.string() }); const schema = z.object({ x: inner, y: inner }); let calls = 0; visit(schema, (s) => { calls++; return s; }); // 3 unique nodes: schema, inner (shared), inner.shape.a (z.string()). expect(calls).toBe(3); }); test("root is cached: lazy self-reference does not re-invoke fn at parse-time", () => { let calls = 0; const Self: z.ZodType = z.object({ self: z.lazy(() => Self).optional() }); const result = visit(Self, (s) => { calls++; return s; }) as z.ZodType; const baseline = calls; result.parse({ self: { self: { self: {} } } }); expect(calls).toBe(baseline); }); test("unknown def.type falls through unchanged (handler map form)", () => { const customSchema: any = z.string(); customSchema._zod.def = { ...customSchema._zod.def, type: "myCustomType" }; const result = visit(customSchema, { object: (o) => o }); expect(result).toBe(customSchema); }); test("non-lazy (getter) cycle resolves through the cache instead of throwing", () => { const Node = z.object({ name: z.string(), get children() { return z.array(Node); }, }); // The rewrite has to reach the recursive reference, not just the top level. const upper = visit(Node, { string: () => z.string().toUpperCase() }) as z.ZodType; expect(upper.parse({ name: "a", children: [{ name: "b", children: [] }] })).toEqual({ name: "A", children: [{ name: "B", children: [] }], }); }); test("catchall is traversed like tuple rest", () => { const schema = z.object({}).catchall(z.object({ n: z.string() })); const result = visit(schema, { string: () => z.string().toUpperCase() }) as z.ZodType; expect(result.parse({ k: { n: "a" } })).toEqual({ k: { n: "A" } }); }); test("an already-resolved lazy is still rewritten", () => { const make = () => z.object({ x: z.lazy(() => z.object({ y: z.string() })) }); const cold = make(); const warm = make(); warm.safeParse({ x: { y: "a" } }); // memoizes $ZodLazy's inner type onto the def // The memo must not ride along into the clone, or the new getter is never read. expect(deepPartial(warm).safeParse({ x: {} }).success).toEqual(true); expect(deepPartial(cold).safeParse({ x: {} }).success).toEqual(true); }); });