Capítulo 56 de 61

Chapter 56: Migration Guide — Number, String, Coercion & Defaults

Core Idea

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).

Reference Tables

AreaZod 3 behaviorZod 4 behavior
z.number()accepted Infinity/-Infinityrejects non-finite values
.safe()distinct from .int(), allowed some float behaviornow 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 matchingstrict RFC 9562/4122 (variant bits 10); use z.guid() for permissive "UUID-like" matching
.base64url()padding allowedpadding no longer allowed
.ip() / .cidr()single method covering v4+v6dropped; use separate z.ipv4()/z.ipv6() or z.cidrv4()/z.cidrv6(), combined via z.union() if needed
z.coerce.* input typetyped as the coerced-from typeunknown
z.coerce.* missing object keysilently 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 pipelinevalue must match the output type; short-circuits without re-parsing (.prefault() added for the old re-parsing behavior)

Code Examples

// 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)
  • What it demonstrates: the .default()/.prefault() split that replicates old vs. new semantics, and the coercion missing-key behavior change.

Key Takeaways

  1. If code relied on .default() re-parsing its value through transforms (the Zod 3 behavior), switch it to .prefault() — plain .default() in Zod 4 now bypasses the pipeline.
  2. Any z.coerce.* field on an object that used to rely on a missing key silently coercing to a fallback now needs an explicit .default().
  3. Migrate .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.

Connects To

  • Defaults, Prefaults & Catch: the current-state documentation of the .default()/.prefault() split introduced here.
  • String Formats — Identifiers, URLs & Contact Info: the current top-level format APIs these methods migrate to.