💎 Zod 4 is now stable!  Read the announcement.
Zod logo

Guía de migración

Esta guía de migración tiene como objetivo enumerar los cambios disruptivos (breaking changes) en Zod 4 en orden de mayor a menor impacto. Para obtener más información sobre las mejoras de rendimiento y las nuevas características de Zod 4, lee la publicación introductoria.

npm install zod@^4.0.0

Muchos de los comportamientos y APIs de Zod se han vuelto más intuitivos y cohesivos. Los cambios disruptivos descritos en este documento a menudo representan mejoras importantes en la calidad de vida para los usuarios de Zod. Recomiendo encarecidamente leer esta guía detenidamente.

Nota — Zod 3 exportaba una serie de tipos y funciones de utilidad cuasi-internos no documentados que no se consideran parte de la API pública. Los cambios en estos no se documentan aquí.

Codemod no oficial — Un codemod mantenido por la comunidad zod-v3-to-v4 está disponible.

Personalización de errores (Error customization)

Zod 4 estandariza las APIs para la personalización de errores bajo un único parámetro unificado error. Anteriormente, las APIs de personalización de errores de Zod estaban fragmentadas y eran inconsistentes. Esto se ha limpiado en Zod 4.

message obsoleto

Reemplaza message con error. El parámetro message todavía es compatible pero está obsoleto (deprecated).

z.string().min(5, { error: "Too short." });

elimina invalid_type_error y required_error

Los parámetros invalid_type_error / required_error han sido eliminados. Estos se agregaron apresuradamente hace años como una forma de personalizar errores que fuera menos verbosa que errorMap. Venían con todo tipo de problemas (no se pueden usar junto con errorMap) y no se alinean con los códigos de problemas reales de Zod (no existe un código de problema required).

Estos ahora se pueden representar limpiamente con el nuevo parámetro error.

z.string({ 
  error: (issue) => issue.input === undefined 
    ? "This field is required" 
    : "Not a string" 
});

elimina errorMap

Esto ha sido renombrado a error.

Los mapas de errores (error maps) ahora también pueden devolver un string simple (en lugar de {message: string}). También pueden devolver undefined, lo que le dice a Zod que ceda el control al siguiente mapa de errores en la cadena.

z.string().min(5, {
  error: (issue) => {
    if (issue.code === "too_small") {
      return `Value must be >${issue.minimum}`
    }
  },
});

ZodError

actualiza los formatos de issue

Los formatos de issue se han simplificado drásticamente.

import * as z from "zod"; // v4
 
type IssueFormats = 
  | z.core.$ZodIssueInvalidType
  | z.core.$ZodIssueTooBig
  | z.core.$ZodIssueTooSmall
  | z.core.$ZodIssueInvalidStringFormat
  | z.core.$ZodIssueNotMultipleOf
  | z.core.$ZodIssueUnrecognizedKeys
  | z.core.$ZodIssueInvalidValue
  | z.core.$ZodIssueInvalidUnion
  | z.core.$ZodIssueInvalidKey // nuevo: usado para z.record/z.map 
  | z.core.$ZodIssueInvalidElement // nuevo: usado para z.map/z.set
  | z.core.$ZodIssueCustom;

A continuación se muestra la lista de tipos de issues de Zod 3 y su equivalente en Zod 4:

import * as z from "zod"; // v3
 
export type IssueFormats =
  | z.ZodInvalidTypeIssue // ♻️ renombrado a z.core.$ZodIssueInvalidType
  | z.ZodTooBigIssue  // ♻️ renombrado a z.core.$ZodIssueTooBig
  | z.ZodTooSmallIssue // ♻️ renombrado a z.core.$ZodIssueTooSmall
  | z.ZodInvalidStringIssue // ♻️ z.core.$ZodIssueInvalidStringFormat
  | z.ZodNotMultipleOfIssue // ♻️ renombrado a z.core.$ZodIssueNotMultipleOf
  | z.ZodUnrecognizedKeysIssue // ♻️ renombrado a z.core.$ZodIssueUnrecognizedKeys
  | z.ZodInvalidUnionIssue // ♻️ renombrado a z.core.$ZodIssueInvalidUnion
  | z.ZodCustomIssue // ♻️ renombrado a z.core.$ZodIssueCustom
  | z.ZodInvalidEnumValueIssue // ❌ fusionado en z.core.$ZodIssueInvalidValue
  | z.ZodInvalidLiteralIssue // ❌ fusionado en z.core.$ZodIssueInvalidValue
  | z.ZodInvalidUnionDiscriminatorIssue // ❌ lanza un Error en el momento de crear el esquema
  | z.ZodInvalidArgumentsIssue // ❌ z.function lanza ZodError directamente
  | z.ZodInvalidReturnTypeIssue // ❌ z.function lanza ZodError directamente
  | z.ZodInvalidDateIssue // ❌ fusionado en invalid_type
  | z.ZodInvalidIntersectionTypesIssue // ❌ eliminado (lanza un Error regular)
  | z.ZodNotFiniteIssue // ❌ valores infinitos ya no aceptados (invalid_type)

Si bien ciertos tipos de issues de Zod 4 se han fusionado, eliminado o modificado, cada issue sigue siendo estructuralmente similar a su contraparte de Zod 3 (idéntico, en la mayoría de los casos). Todos los issues todavía se ajustan a la misma interfaz base que Zod 3, por lo que la mayoría de la lógica de manejo de errores común funcionará sin modificaciones.

export interface $ZodIssueBase {
  readonly code?: string;
  readonly input?: unknown;
  readonly path: PropertyKey[];
  readonly message: string;
}

cambia la precedencia del mapa de errores

La precedencia del mapa de errores se ha cambiado para ser más consistente. Específicamente, un mapa de errores pasado a .parse() ya no tiene prioridad sobre un mapa de errores a nivel de esquema.

const mySchema = z.string({ error: () => "Schema-level error" });
 
// en Zod 3
mySchema.parse(12, { error: () => "Contextual error" }); // => "Contextual error"
 
// en Zod 4
mySchema.parse(12, { error: () => "Contextual error" }); // => "Schema-level error"

obsoleto .format()

El método .format() en ZodError ha quedado obsoleto. En su lugar, usa la función de nivel superior z.treeifyError(). Lee la documentación de Formateo de errores para más información.

obsoleto .flatten()

El método .flatten() en ZodError también ha quedado obsoleto. En su lugar, usa la función de nivel superior z.treeifyError(). Lee la documentación de Formateo de errores para más información.

elimina .formErrors

Esta API era idéntica a .flatten(). Existe por razones históricas y no está documentada.

obsoleto .addIssue() y .addIssues()

Empuja directamente al array err.issues en su lugar, si es necesario.

myError.issues.push({ 
  // nuevo issue
});

z.number()

sin valores infinitos

POSITIVE_INFINITY y NEGATIVE_INFINITY ya no se consideran valores válidos para z.number().

.safe() ya no acepta flotantes

En Zod 3, z.number().safe() está obsoleto. Ahora se comporta de manera idéntica a .int() (ver a continuación). Es importante destacar que eso significa que ya no acepta flotantes (números decimales).

.int() acepta solo enteros seguros

La API z.number().int() ya no acepta enteros inseguros (fuera del rango de Number.MIN_SAFE_INTEGER y Number.MAX_SAFE_INTEGER). Usar enteros fuera de este rango causa errores de redondeo espontáneos. (Además: deberías cambiar a z.int().)

actualizaciones de z.string()

obsoleto .email() etc

Los formatos de cadena ahora se representan como subclases de ZodString, en lugar de simples refinamientos internos. Como tal, estas APIs se han movido al espacio de nombres de nivel superior z. Las APIs de nivel superior también son menos verbosas y más fáciles de hacer "tree-shaking".

z.email();
z.uuid();
z.url();
z.emoji();         // valida un solo carácter emoji
z.base64();
z.base64url();
z.nanoid();
z.cuid();
z.cuid2();
z.ulid();
z.ipv4();
z.ipv6();
z.cidrv4();          // rango de ip
z.cidrv6();          // rango de ip
z.iso.date();
z.iso.time();
z.iso.datetime();
z.iso.duration();

Las formas de método (z.string().email()) todavía existen y funcionan como antes, pero ahora están obsoletas.

z.string().email(); // ❌ obsoleto
z.email(); // ✅ 

.uuid() más estricto

z.uuid() ahora valida los UUIDs más estrictamente contra la especificación RFC 9562/4122; específicamente, los bits de variante deben ser 10 según la especificación. Para un validador "tipo UUID" más permisivo, usa z.guid().

z.uuid(); // UUID compatible con RFC 9562/4122
z.guid(); // cualquier patrón hexadecimal 8-4-4-4-12

sin relleno en .base64url()

El relleno (padding) ya no está permitido en z.base64url() (anteriormente z.string().base64url()). Generalmente es deseable que las cadenas base64url no tengan relleno y sean seguras para URL.

elimina z.string().ip()

Esto ha sido reemplazado con métodos separados .ipv4() y .ipv6(). Usa z.union() para combinarlos si necesitas aceptar ambos.

z.string().ip() // ❌
z.ipv4() // ✅
z.ipv6() // ✅

actualiza z.string().ipv6()

La validación ahora ocurre usando el constructor new URL(), que es mucho más robusto que el antiguo enfoque de expresiones regulares. Algunos valores inválidos que pasaban la validación anteriormente pueden fallar ahora.

elimina z.string().cidr()

De manera similar, esto ha sido reemplazado con métodos separados .cidrv4() y .cidrv6(). Usa z.union() para combinarlos si necesitas aceptar ambos.

z.string().cidr() // ❌
z.cidrv4() // ✅
z.cidrv6() // ✅

actualizaciones de z.coerce

El tipo de entrada de todos los esquemas z.coerce ahora es unknown.

const schema = z.coerce.string();
type schemaInput = z.input<typeof schema>;
 
// Zod 3: string;
// Zod 4: unknown;

actualizaciones de .default()

La aplicación de .default() ha cambiado de manera sutil. Si la entrada es undefined, ZodDefault cortocircuita el proceso de análisis y devuelve el valor predeterminado. El valor predeterminado debe ser asignable al tipo de salida.

const schema = z.string()
  .transform(val => val.length)
  .default(0); // debería ser un número
schema.parse(undefined); // => 0

En Zod 3, .default() esperaba un valor que coincidiera con el tipo de entrada. ZodDefault analizaría el valor predeterminado, en lugar de cortocircuitar. Como tal, el valor predeterminado debe ser asignable al tipo de entrada del esquema.

// Zod 3
const schema = z.string()
  .transform(val => val.length)
  .default("tuna");
schema.parse(undefined); // => 4

Para replicar el comportamiento antiguo, Zod implementa una nueva API .prefault(). Esto es una abreviatura de "pre-parse default" (predeterminado pre-análisis).

// Zod 3
const schema = z.string()
  .transform(val => val.length)
  .prefault("tuna");
schema.parse(undefined); // => 4

z.object()

valores predeterminados aplicados dentro de campos opcionales

Los valores predeterminados dentro de tus propiedades se aplican, incluso dentro de campos opcionales. Esto se alinea mejor con las expectativas y resuelve un problema de usabilidad de larga data con Zod 3. Este es un cambio sutil que puede causar roturas en rutas de código que dependen de la existencia de claves, etc.

const schema = z.object({
  a: z.string().default("tuna").optional(),
});
 
schema.parse({});
// Zod 4: { a: "tuna" }
// Zod 3: {}

obsoleto .strict() y .passthrough()

Estos métodos generalmente ya no son necesarios. En su lugar, usa las funciones de nivel superior z.strictObject() y z.looseObject().

// Zod 3
z.object({ name: z.string() }).strict();
z.object({ name: z.string() }).passthrough();
 
// Zod 4
z.strictObject({ name: z.string() });
z.looseObject({ name: z.string() });

Estos métodos todavía están disponibles por compatibilidad con versiones anteriores, y no se eliminarán. Se consideran heredados (legacy).

obsoleto .strip()

Esto nunca fue particularmente útil, ya que era el comportamiento predeterminado de z.object(). Para convertir un objeto estricto en uno "regular", usa z.object(A.shape).

elimina .nonstrict()

Este alias largamente obsoleto para .strip() ha sido eliminado.

elimina .deepPartial()

Esto ha estado obsoleto desde hace mucho tiempo en Zod 3 y ahora se elimina en Zod 4. No hay una alternativa directa a esta API. Había muchos problemas en su implementación, y su uso es generalmente un anti-patrón.

cambia la opcionalidad de z.unknown()

Los tipos z.unknown() y z.any() ya no están marcados como "clave opcional" en los tipos inferidos.

const mySchema = z.object({
  a: z.any(),
  b: z.unknown()
});
// Zod 3: { a?: any; b?: unknown };
// Zod 4: { a: any; b: unknown };

obsoleto .merge()

El método .merge() en ZodObject ha quedado obsoleto a favor de .extend(). El método .extend() proporciona la misma funcionalidad, evita la ambigüedad en torno a la herencia de rigurosidad (strictness), y tiene un mejor rendimiento en TypeScript.

// .merge (obsoleto)
const ExtendedSchema = BaseSchema.merge(AdditionalSchema);
 
// .extend (recomendado)
const ExtendedSchema = BaseSchema.extend(AdditionalSchema.shape);
 
// o usa desestructuración (mejor rendimiento tsc)
const ExtendedSchema = z.object({
  ...BaseSchema.shape,
  ...AdditionalSchema.shape,
});

Nota: Para un rendimiento de TypeScript aún mejor, considera usar la desestructuración de objetos en lugar de .extend(). Consulta la documentación de la API para más detalles.

z.nativeEnum() obsoleto

La función z.nativeEnum() ahora está obsoleta a favor de simplemente z.enum(). La API z.enum() se ha sobrecargado para soportar una entrada tipo enum.

enum Color {
  Red = "red",
  Green = "green",
  Blue = "blue",
}
 
const ColorSchema = z.enum(Color); // ✅

Como parte de esta refactorización de ZodEnum, se han eliminado una serie de características largamente obsoletas y redundantes. Todas eran idénticas y solo existían por razones históricas.

ColorSchema.enum.Red; // ✅ => "Red" (API canónica)
ColorSchema.Enum.Red; // ❌ eliminado
ColorSchema.Values.Red; // ❌ eliminado

z.array()

cambia el tipo .nonempty()

Esto ahora se comporta de manera idéntica a z.array().min(1). El tipo inferido no cambia.

const NonEmpty = z.array(z.string()).nonempty();
 
type NonEmpty = z.infer<typeof NonEmpty>; 
// Zod 3: [string, ...string[]]
// Zod 4: string[]

El comportamiento antiguo ahora se representa mejor con z.tuple() y un argumento "rest". Esto se alinea más estrechamente con el sistema de tipos de TypeScript.

z.tuple([z.string()], z.string());
// => [string, ...string[]]

z.promise() obsoleto

Rara vez hay una razón para usar z.promise(). Si tienes una entrada que puede ser una Promise, simplemente usa await antes de analizarla con Zod.

Si estás usando z.promise para definir una función asíncrona con z.function(), eso tampoco es necesario ya; consulta la sección ZodFunction a continuación.

z.function()

El resultado de z.function() ya no es un esquema Zod. En cambio, actúa como una "fábrica de funciones" independiente para definir funciones validadas por Zod. La API también ha cambiado; defines un esquema de input y output por adelantado, en lugar de usar los métodos args() y .returns().

const myFunction = z.function({
  input: [z.object({
    name: z.string(),
    age: z.number().int(),
  })],
  output: z.string(),
});
 
myFunction.implement((input) => {
  return `Hello ${input.name}, you are ${input.age} years old.`;
});

Si tienes una necesidad desesperada de un esquema Zod con un tipo de función, considera esta solución alternativa.

agrega .implementAsync()

Para definir una función asíncrona, usa implementAsync() en lugar de implement().

myFunction.implementAsync(async (input) => {
  return `Hello ${input.name}, you are ${input.age} years old.`;
});

.refine()

ignora los predicados de tipo

En Zod 3, pasar un predicado de tipo como una función de refinamiento aún podía reducir el tipo de un esquema. Esto no estaba documentado pero se discutió en algunos problemas. Este ya no es el caso.

const mySchema = z.unknown().refine((val): val is string => {
  return typeof val === "string"
});
 
type MySchema = z.infer<typeof mySchema>; 
// Zod 3: `string`
// Zod 4: todavía `unknown`

elimina ctx.path

La nueva arquitectura de análisis de Zod no evalúa ansiosamente el array path. Este fue un cambio necesario que desbloquea las dramáticas mejoras de rendimiento de Zod 4.

z.string().superRefine((val, ctx) => {
  ctx.path; // ❌ ya no está disponible
});

elimina la función como segundo argumento

La siguiente sobrecarga horrorosa ha sido eliminada.

const longString = z.string().refine(
  (val) => val.length > 10,
  (val) => ({ message: `${val} is not more than 10 characters` })
);

z.ostring(), etc eliminados

Los métodos de conveniencia no documentados z.ostring(), z.onumber(), etc. han sido eliminados. Estos eran métodos abreviados para definir esquemas de cadenas opcionales.

z.literal()

elimina el soporte de symbol

Los Símbolos no se consideran valores literales, ni pueden compararse simplemente con ===. Esto fue un descuido en Zod 3.

fábricas estáticas .create() eliminadas

Anteriormente, todas las clases Zod definían un método estático .create(). Estos ahora se implementan como funciones de fábrica independientes.

z.ZodString.create(); // ❌ 

z.record()

elimina el uso de un solo argumento

Antes, z.record() podía usarse con un solo argumento. Esto ya no es compatible.

// Zod 3
z.record(z.string()); // ✅
 
// Zod 4
z.record(z.string()); // ❌
z.record(z.string(), z.string()); // ✅

mejora el soporte de enum

Los registros (Records) se han vuelto mucho más inteligentes. En Zod 3, pasar un enum a z.record() como un esquema clave resultaría en un tipo parcial.

const myRecord = z.record(z.enum(["a", "b", "c"]), z.number()); 
// { a?: number; b?: number; c?: number; }

En Zod 4, este ya no es el caso. El tipo inferido es lo que esperarías, y Zod asegura la exhaustividad; es decir, se asegura de que todas las claves enum existan en la entrada durante el análisis.

const myRecord = z.record(z.enum(["a", "b", "c"]), z.number());
// { a: number; b: number; c: number; }

Para replicar el comportamiento antiguo con claves opcionales, usa z.partialRecord():

const myRecord = z.partialRecord(z.enum(["a", "b", "c"]), z.number());
// { a?: number; b?: number; c?: number; }

z.intersection()

lanza Error en conflicto de fusión

La intersección de Zod analiza la entrada contra dos esquemas, luego intenta fusionar los resultados. En Zod 3, cuando los resultados no eran fusionables, Zod lanzaba un ZodError con un problema especial "invalid_intersection_types".

En Zod 4, esto lanzará un Error regular en su lugar. La existencia de resultados no fusionables indica un problema estructural con el esquema: una intersección de dos tipos incompatibles. Por lo tanto, un error regular es más apropiado que un error de validación.

Cambios internos

El usuario típico de Zod probablemente puede ignorar todo lo que está debajo de esta línea. Estos cambios no afectan a las APIs z orientadas al usuario.

Hay demasiados cambios internos para enumerar aquí, pero algunos pueden ser relevantes para los usuarios habituales que dependen (intencionalmente o no) de ciertos detalles de implementación. Estos cambios serán de particular interés para los autores de bibliotecas que crean herramientas sobre Zod.

actualizaciones de genéricos

La estructura genérica de varias clases ha cambiado. Quizás lo más significativo es el cambio a la clase base ZodType:

// Zod 3
class ZodType<Output, Def extends z.ZodTypeDef, Input = Output> {
  // ...
}
 
// Zod 4
class ZodType<Output = unknown, Input = unknown> {
  // ...
}

El segundo genérico Def ha sido eliminado por completo. En cambio, la clase base ahora solo rastrea Output e Input. Mientras que anteriormente el valor Input predeterminado era Output, ahora predeterminado es unknown. Esto permite que las funciones genéricas que involucran z.ZodType se comporten de manera más intuitiva en muchos casos.

function inferSchema<T extends z.ZodType>(schema: T): T {
  return schema;
};
 
inferSchema(z.string()); // z.ZodString

La necesidad de z.ZodTypeAny ha sido eliminada; solo usa z.ZodType en su lugar.

agrega z.core

Muchas funciones y tipos de utilidad se han movido al nuevo sub-paquete zod/v4/core, para facilitar el intercambio de código entre Zod y Zod Mini.

import * as z from "zod/v4/core";
 
function handleError(iss: z.$ZodError) {
  // hacer cosas
}

Para mayor comodidad, el contenido de zod/v4/core también se reexporta desde zod y zod/mini bajo el espacio de nombres z.core.

import * as z from "zod";
 
function handleError(iss: z.core.$ZodError) {
  // hacer cosas
}

Consulta la documentación de Zod Core para obtener más información sobre el contenido de la sub-biblioteca principal.

mueve ._def

La propiedad ._def ahora se mueve a ._zod.def. La estructura de todas las defs internas está sujeta a cambios; esto es relevante para los autores de bibliotecas pero no se documentará exhaustivamente aquí.

elimina ZodEffects

Esto no afecta a las APIs orientadas al usuario, pero es un cambio interno que vale la pena destacar. Es parte de una reestructuración más grande de cómo Zod maneja los refinamientos.

Anteriormente, tanto los refinamientos como las transformaciones vivían dentro de una clase contenedora llamada ZodEffects. Eso significa que agregar cualquiera de los dos a un esquema envolvería el esquema original en una instancia de ZodEffects. En Zod 4, los refinamientos ahora viven dentro de los propios esquemas. Más exactamente, cada esquema contiene una matriz de "checks" (comprobaciones); el concepto de un "check" es nuevo en Zod 4 y generaliza el concepto de un refinamiento para incluir transformaciones potencialmente con efectos secundarios como z.toLowerCase().

Esto es particularmente evidente en la API Zod Mini, que depende en gran medida del método .check() para componer varias validaciones juntas.

import * as z from "zod/mini";
 
z.string().check(
  z.minLength(10),
  z.maxLength(100),
  z.toLowerCase(),
  z.trim(),
);

agrega ZodTransform

Mientras tanto, las transformaciones se han movido a una clase dedicada ZodTransform. Esta clase de esquema representa una transformación de entrada; de hecho, ahora puedes definir transformaciones independientes:

import * as z from "zod";
 
const schema = z.transform(input => String(input));
 
schema.parse(12); // => "12"

Esto se usa principalmente junto con ZodPipe. El método .transform() ahora devuelve una instancia de ZodPipe.

z.string().transform(val => val); // ZodPipe<ZodString, ZodTransform>

elimina ZodPreprocess

Al igual que con .transform(), la función z.preprocess() ahora devuelve una instancia de ZodPipe en lugar de una instancia dedicada de ZodPreprocess.

z.preprocess(val => val, z.string()); // ZodPipe<ZodTransform, ZodString>

elimina ZodBranded

El branding ahora se maneja con una modificación directa al tipo inferido, en lugar de una clase dedicada ZodBranded. Las APIs orientadas al usuario permanecen iguales.

On this page