Capítulo 56 de 61
Zod 4 tightens several primitive-schema behaviors that were previously loose or surprising in Zod 3: numbers reject infinities, string formats move to top-level tree-shakable functions, coercion input types widen to unknown, and .default() semantics change to short-circuit rather than re-parse (with .prefault() added to recover the old behavior).
| Area | Zod 3 behavior | Zod 4 behavior |
|---|---|---|
z.number() | accepted Infinity/-Infinity | rejects non-finite values |
.safe() | distinct from .int(), allowed some float behavior | now identical to .int(), safe-integer range only |
.int() | accepted unsafe integers (outside MIN/MAX_SAFE_INTEGER) | safe integers only |
| String formats | .email(), .uuid(), .url(), etc. as methods on z.string() | top-level z.email(), z.uuid(), etc. (tree-shakable subclasses); method forms still work but are deprecated |
z.uuid() | looser UUID matching | strict RFC 9562/4122 (variant bits 10); use z.guid() for permissive "UUID-like" matching |
.base64url() | padding allowed | padding no longer allowed |
.ip() / .cidr() | single method covering v4+v6 | dropped; use separate z.ipv4()/z.ipv6() or z.cidrv4()/z.cidrv6(), combined via z.union() if needed |
z.coerce.* input type | typed as the coerced-from type | unknown |
z.coerce.* missing object key | silently defaulted (e.g. false for boolean) | now errors — use .default() to declare a fallback explicitly |
.default() | value must match the schema's input type; re-parses the default through the pipeline | value must match the output type; short-circuits without re-parsing (.prefault() added for the old re-parsing behavior) |
// string formats: prefer top-level functions
z.string().email(); // deprecated
z.email(); // preferred
// .default() vs .prefault() — Zod 4 semantics
const withDefault = z.string().transform(v => v.length).default(0); // output-typed, short-circuits
withDefault.parse(undefined); // => 0
const withPrefault = z.string().transform(v => v.length).prefault("tuna"); // input-typed, still runs pipeline
withPrefault.parse(undefined); // => 4 (old Zod 3 .default() behavior)
// coerce missing-key change
z.object({ foo: z.coerce.boolean() }).parse({});
// Zod 3: { foo: false } — Zod 4: throws (declare .default() explicitly if that's wanted)
.default()/.prefault() split that replicates old vs. new semantics, and the coercion missing-key behavior change..default() re-parsing its value through transforms (the Zod 3 behavior), switch it to .prefault() — plain .default() in Zod 4 now bypasses the pipeline.z.coerce.* field on an object that used to rely on a missing key silently coercing to a fallback now needs an explicit .default()..string().<format>() calls to the top-level z.<format>() equivalents — both work, but only the top-level form is fully tree-shakable and considered current..default()/.prefault() split introduced here.