Capítulo 57 de 61
Zod 4 makes object/enum/array/function APIs more consistent and TypeScript-sound: defaults now apply inside optional fields, .strict()/.passthrough()/.merge() are superseded by clearer top-level equivalents, z.function() becomes a factory instead of a schema, and .refine() no longer narrows types via type predicates.
| Area | Zod 3 behavior | Zod 4 behavior |
|---|---|---|
| Defaults inside optional object fields | not applied — z.object({ a: z.string().default("tuna").optional() }).parse({}) → {} | applied — same call → { a: "tuna" } |
.strict() / .passthrough() | instance methods | deprecated (still work) in favor of z.strictObject() / z.looseObject() |
.strip() | instance method | deprecated — was already the default; use z.object(A.shape) to convert |
.nonstrict() | deprecated alias for .strip() | removed |
.deepPartial() | deprecated | removed entirely, no direct replacement |
z.any()/z.unknown() object keys | inferred as optional | required (key must exist, value may be undefined); enforced at parse time as of v4.4.0 |
.merge() | method for combining object schemas | deprecated in favor of .extend() or object spread (better tsc performance, no strictness ambiguity) |
z.nativeEnum() | separate API for TS enums | deprecated — z.enum() now accepts TS enums directly; redundant .Enum/.Values aliases removed, .enum is canonical |
z.array().nonempty() | inferred type [T, ...T[]] | inferred type is plain T[] (use z.tuple([T], T) for the old tuple-shaped type) |
z.promise() | usable schema | deprecated — await the value before parsing instead |
z.function() | a ZodFunction schema built via .args()/.returns() | a factory taking { input, output } upfront, not itself a schema; adds .implementAsync() |
.refine() with a type-predicate function | could narrow the schema's inferred type (undocumented) | no longer narrows — the schema's type stays as declared |
// defaults now apply inside optional fields
z.object({ a: z.string().default("tuna").optional() }).parse({});
// Zod 4: { a: "tuna" } (Zod 3: {})
// strict/loose object: prefer the top-level functions
z.strictObject({ name: z.string() });
z.looseObject({ name: z.string() });
// merge → extend or spread
const Extended = z.object({ ...BaseSchema.shape, ...AdditionalSchema.shape });
// nativeEnum → enum accepts TS enums directly
enum Color { Red = "red", Green = "green" }
const ColorSchema = z.enum(Color);
ColorSchema.enum.Red; // canonical accessor — .Enum/.Values removed
// z.function() is now a factory, not a schema
const myFunction = z.function({
input: [z.object({ name: z.string(), age: z.number().int() })],
output: z.string(),
});
myFunction.implement((input) => `Hello ${input.name}`);
.deepPartial(): removed with no direct replacement — restructure the schema or write the recursive partial logic explicitly if truly needed..refine() type predicates to narrow a schema's type: no longer works — use z.custom<T>() or restructure the schema if type narrowing is actually required..default() and .optional() on the same field after upgrading.z.function() no longer being a schema is a structural change, not just a rename — code treating its result as a ZodType (e.g. passing it to .optional()) needs rewriting.z.enum() fully subsumes z.nativeEnum() now — there's no remaining reason to reach for the deprecated API..extend()/.safeExtend() APIs that replace .merge().z.function()/.implement() API described here.