Capítulo 58 de 61
Several smaller-surface-area APIs changed shape in Zod 4: ctx.path was dropped from refinement context for performance reasons, z.record() now always requires an explicit key schema, enum-keyed records became exhaustive by default, and intersection merge conflicts throw a plain Error instead of a ZodError.
| Area | Zod 3 behavior | Zod 4 behavior |
|---|---|---|
ctx.path in .superRefine() | available | removed — eager path evaluation was incompatible with Zod 4's performance architecture |
.refine(fn, fnReturningMessage) | second-argument-as-function overload supported | removed — use the error param instead |
z.ostring(), z.onumber(), etc. | undocumented optional-schema shortcuts | removed |
z.literal(someSymbol) | accepted symbols | rejected — symbols aren't literal-comparable |
SomeZodClass.create() | static factory method on every class | removed — use the standalone z.xxx() factory functions |
z.record(valueSchema) | single-argument form (implicit string key) | requires two arguments: z.record(keySchema, valueSchema) |
z.record(z.enum([...]), valueSchema) | inferred type was partial ({ a?: number }) | exhaustive by default ({ a: number }); use z.partialRecord() for the old partial behavior |
| Intersection merge conflict | threw ZodError with "invalid_intersection_types" | throws a plain Error — treated as a schema design problem, not a validation failure |
// z.record() now always needs both key and value schemas
z.record(z.string()); // Zod 3 only — no longer valid
z.record(z.string(), z.string()); // Zod 4
// enum-keyed records are exhaustive by default now
const myRecord = z.record(z.enum(["a", "b", "c"]), z.number());
// Zod 4 inferred type: { a: number; b: number; c: number } — all keys required
// use partialRecord to get the old "maybe has these keys" behavior
const partial = z.partialRecord(z.enum(["a", "b", "c"]), z.number());
// { a?: number; b?: number; c?: number }
z.record() form and the exhaustive-by-default enum-key behavior, with z.partialRecord() as the escape hatch..refine()'s second-argument-function overload for dynamic messages: removed — use { error: (iss) => ... } instead.z.record(): no longer valid; always supply both key and value schemas.z.record() with one argument, or expecting enum-keyed records to be partial, needs updating for Zod 4's stricter, more TypeScript-faithful defaults.ZodError no longer works; catch a plain Error instead.ctx.path removal is a performance trade-off, not an oversight — don't expect it to come back; restructure refinements that depended on it to use the schema structure itself for path context.z.record()/z.partialRecord()/z.looseRecord() API this section migrates toward..refine() API and its error/abort/path params.