Migrando a Zod 4: Guía completa de breaking changes, mejoras de rendimiento y nuevas funcionalidades
Si escribiste TypeScript en los últimos tres años, usaste Zod. Está en tus API routes, tu validación de formularios, tus endpoints de tRPC, tu parser de variables de entorno. Zod está en todos lados, y Zod 4 acaba de cambiar casi todo.
Los números hablan solos: parsing de strings 14x más rápido, arrays 7x más rápido, bundle core 2.3x más chico, y compilación de TypeScript hasta 10x más rápida. Pero esas mejoras vienen con una lista larga de breaking changes que van a pintar tu CI de rojo en cuanto bumpeés la versión.
Esta guía cubre cada breaking change, te muestra el código before/after exacto, y te da una estrategia de migración para que tu equipo no pase una semana debuggeando errores de schema.
Por qué cambió tanto
Zod 3 fue diseñado cuando el sistema de tipos de TypeScript era menos capaz y el tamaño del bundle no era una preocupación central para librerías de validación server-side. A medida que el ecosistema evolucionó (funciones serverless con cold starts, Edge runtimes con límites de tamaño estrictos, monorepos con miles de schemas), la arquitectura de Zod 3 empezó a mostrar sus límites.
Zod 4 es una reescritura desde cero que ataca tres problemas fundamentales:
- Tamaño del bundle: La API de method-chaining de Zod 3 hacía que el tree-shaking fuera prácticamente imposible. Un solo import arrastraba la librería entera.
- Performance de parsing: El pipeline de validación tenía overhead innecesario por creación de objetos y lookups en la cadena de prototipos.
- Velocidad de compilación TypeScript: Schemas complejos de Zod generaban cantidades enormes de type instantiations, haciendo que
tscse arrastrara en codebases grandes.
Para resolver esto de raíz, había que romper la API. Vamos una por una.
Breaking Change #1: Parámetro de error unificado
Este es el cambio que más código va a romper. Zod 3 tenía tres formas diferentes de personalizar mensajes de error:
// ❌ Zod 3 — Tres parámetros distintos const schema = z.string({ required_error: "El nombre es obligatorio", invalid_type_error: "El nombre debe ser un string", }); const email = z.string().email({ message: "Email inválido" }); const age = z.number({ errorMap: (issue, ctx) => { if (issue.code === "too_small") return { message: "Debe ser mayor de 18" }; return { message: ctx.defaultError }; }, });
Zod 4 reemplaza todo con un único parámetro error:
// ✅ Zod 4 — Parámetro error unificado const schema = z.string({ error: "El nombre es obligatorio", }); const email = z.string().email({ error: "Email inválido", }); const age = z.number({ error: (issue) => { if (issue.code === "too_small") return "Debe ser mayor de 18"; return "Edad inválida"; }, });
La propiedad message ahora está deprecated en todos los métodos. required_error, invalid_type_error y errorMap desaparecieron por completo. Usá el parámetro unificado error en todas partes. El codemod oficial maneja la mayoría:
npx @zod/codemod --transform v3-to-v4 ./src
Los errorMap custom necesitan revisión manual porque la firma cambió de (issue, ctx) => { message: string } a (issue) => string.
Breaking Change #2: Validadores de formato top-level
Zod 3 usaba method chaining para validación de formatos de string. Zod 4 promueve los más comunes a funciones top-level:
// ❌ Zod 3 — Method chaining const emailSchema = z.string().email(); const uuidSchema = z.string().uuid(); const urlSchema = z.string().url(); // ✅ Zod 4 — Funciones top-level const emailSchema = z.email(); const uuidSchema = z.uuid(); const urlSchema = z.url();
z.string().email() arrastra toda la clase ZodString aunque solo necesites validar emails. Las funciones top-level permiten tree-shaking real.
Importante: Las versiones con method-chain (z.string().email()) siguen funcionando pero están oficialmente deprecated en Zod 4. No se van a eliminar de inmediato, así que podés migrar gradualmente, pero esperá que se droppen en una futura versión major.
También: z.string().ip() y z.string().cidr() se eliminaron completamente, reemplazados por z.ipv4(), z.ipv6(), z.cidrv4() y z.cidrv6(). Y z.uuid() ahora es más estricto, validando variant bits de RFC 9562/4122. Si necesitás un patrón más permisivo, usá el nuevo z.guid().
Breaking Change #3: Los tipos de input de Coercion ahora son unknown
El namespace z.coerce sigue existiendo en Zod 4, pero el tipo de input de todos los schemas coercionados cambió del tipo específico a unknown:
const schema = z.coerce.string(); type SchemaInput = z.input<typeof schema>; // Zod 3: string // Zod 4: unknown
Esto refleja mejor lo que la coerción realmente hace: acepta cualquier cosa e intenta convertirla. Pero significa que TypeScript ya no va a narrowear el tipo de input, lo cual puede generar errores de tipo en código que dependía del tipo narroweado.
El namespace z.coerce en sí (z.coerce.number(), z.coerce.string(), etc.) sigue funcionando como antes: la superficie de API no cambió, solo el tipo de input inferido.
Breaking Change #4: Cambio en el comportamiento de Optional + Default
Sutil pero peligroso. En Zod 3, llamar a .optional() sobre un schema con .default() o .catch() ignoraba propiedades faltantes. En Zod 4, el valor default se aplica siempre:
const schema = z.object({ theme: z.string().default("light").optional(), }); // Zod 3: { theme: undefined } → { theme: undefined } ← Propiedad faltante ignorada // Zod 4: { theme: undefined } → { theme: "light" } ← Default aplicado
El comportamiento es más predecible, pero puede romper código que chequea undefined para detectar "no provisto".
Otro cambio en .default(): el valor default ahora debe matchear el tipo de output, no el de input. En Zod 3, .default() parseaba el valor default a través del schema. En Zod 4, hace short-circuit y devuelve el default directamente:
// Zod 3: default matcheaba tipo INPUT, se parseaba const schema = z.string() .transform(val => val.length) .default("tuna"); // string input → parsed → 4 schema.parse(undefined); // => 4 // Zod 4: default matchea tipo OUTPUT, se devuelve directo const schema = z.string() .transform(val => val.length) .default(0); // number output, devuelto directo schema.parse(undefined); // => 0
Para replicar el comportamiento viejo ("pre-parse default"), Zod 4 introduce .prefault():
// ✅ Zod 4 — .prefault() para el comportamiento viejo de .default() const schema = z.string() .transform(val => val.length) .prefault("tuna"); schema.parse(undefined); // => 4
Breaking Change #5: TypeScript Strict mode obligatorio
Zod 4 requiere strict: true en tu tsconfig.json y TypeScript 5.5+:
{ "compilerOptions": { "strict": true, "target": "ES2022", "module": "ESNext" } }
Nueva funcionalidad: @zod/mini
Si deployás en Edge runtimes o funciones serverless donde el tamaño del bundle importa, esto cambia el juego:
import { z } from "@zod/mini"; const UserSchema = z.object({ name: z.string().check(z.minLength(1)), email: z.string().check(z.email()), role: z.enum(["admin", "user", "viewer"]), });
| Funcionalidad | zod | @zod/mini |
|---|---|---|
| Bundle core | ~13KB gzip | ~5.5KB gzip |
.transform() | ✅ | ❌ |
.pipe() | ✅ | ❌ |
| JSON Schema | ✅ | ❌ |
Para API route handlers que solo validan sin transformar, @zod/mini es la opción inteligente.
Nueva funcionalidad: Conversión JSON Schema nativa
No más instalar zod-to-json-schema. Zod 4 trae generación de JSON Schema integrada:
const UserSchema = z.object({ id: z.number().int().positive(), name: z.string().min(1).max(255), email: z.email(), role: z.enum(["admin", "editor", "viewer"]), }); const jsonSchema = UserSchema.toJSONSchema();
Y la dirección inversa también funciona:
const schema = z.fromJSONSchema({ type: "object", properties: { name: { type: "string" }, age: { type: "integer", minimum: 0 }, }, required: ["name"], });
Enorme para interop con specs OpenAPI, generadores de forms basados en JSON Schema y definiciones de AI tools (MCP, function calling).
Nueva funcionalidad: Metadatos de schema
Podés adjuntar metadatos fuertemente tipados a tus schemas:
const NameSchema = z.string().min(1).max(100).meta({ label: "Nombre completo", placeholder: "Juan Pérez", helpText: "Ingresá tu nombre legal.", }); const meta = NameSchema.meta();
Esto habilita patrones como la generación de formularios basada en schemas. Los metadatos se preservan a través de operaciones como .optional(), .array() y .transform().
Nueva funcionalidad: Errores internacionalizados
Zod 4 incluye un sistema de locales para traducir mensajes de error:
import { z } from "zod"; import { es } from "@zod/locales/es"; z.config({ locale: es }); const result = z.string().min(5).safeParse("ho"); // result.error.issues[0].message → "Debe tener al menos 5 caracteres"
Se acabó envolver cada schema con error maps custom solo para soportar múltiples idiomas.
Nueva funcionalidad: Template Literal Types
Zod 4 introduce z.templateLiteral() para validar strings que siguen un patrón específico:
const hexColor = z.templateLiteral([ z.literal("#"), z.string().regex(/^[0-9a-fA-F]{6}$/), ]); hexColor.parse("#ff00aa"); // ✅ hexColor.parse("red"); // ❌ type HexColor = z.infer<typeof hexColor>; // => `#${string}`
Muy útil para validar formatos de strings estructurados como valores CSS, strings de versión semántica o patrones de endpoints de API, con inferencia de tipos TypeScript completa.
Benchmarks de rendimiento
| Operación | Zod 3 | Zod 4 | Mejora |
|---|---|---|---|
| Parse de strings | 1.0x | 14x | 1,300% |
| Parse de arrays | 1.0x | 7x | 600% |
| Parse de objetos | 1.0x | 6.5x | 550% |
| Bundle size | ~31KB gzip | ~13KB gzip | 2.3x más chico |
| TS type instantiations | 1.0x | hasta 10x menos | 900% |
La mejora en compilación de TypeScript es particularmente impactante. En codebases con cientos de schemas, un benchmark reportó que tsc bajó de 47 segundos a 5 segundos.
Estrategia de migración: el camino seguro
No hagas una migración big-bang. Acá va un enfoque por fases:
Fase 1: Preparación
- Verificar que TypeScript strict mode esté habilitado
- Correr el codemod en dry-run:
npx @zod/codemod --transform v3-to-v4 --dry-run ./src
- Buscar
errorMapen el codebase para identificar migraciones manuales - Verificar patrones
.optional().default()(comportamiento diferente en v4) - Confirmar TypeScript 5.5+
Fase 2: Ejecutar el codemod
npx @zod/codemod --transform v3-to-v4 ./src git diff --stat
Fase 3: Correcciones manuales
grep -rn "errorMap" --include="*.ts" --include="*.tsx" ./src grep -rn "z\.coerce\." --include="*.ts" --include="*.tsx" ./src grep -rn "\.optional()\.default\|\.default(.*).optional" --include="*.ts" ./src
Fase 4: Testear
- Cambio en formato de mensajes de error: Tests que assertean mensajes específicos van a fallar
- Tipos de input de coerción: Los tipos de input de
z.coerce.*cambiaron aunknown, lo que puede generar nuevos errores de tipo - Optional+Default: Chequeos de
undefineden campos opcionales con default van a cambiar
Fase 5: Adoptar nuevas features (opcional)
- Reemplazar
zod-to-json-schemapor.toJSONSchema() - Manejar metadatos de schemas con
z.registry() - Migrar schemas de solo validación a
@zod/mini - Usar
z.templateLiteral()para validación de strings estructurados
Gotchas comunes
Gotcha 1: Peer dependencies
Librerías que dependen de Zod 3 pueden no aceptar Zod 4 como peer dependency:
npm ls zod
A marzo 2026, la mayoría de las librerías principales ya soportan Zod 4: tRPC v11+, @tanstack/react-form, react-hook-form v8 + @hookform/resolvers v4+.
Gotcha 2: z.input / z.output cambiaron
z.infer<typeof schema> no cambió. Pero z.input y z.output pueden producir tipos diferentes en v4 por los cambios en defaults y transforms.
Gotcha 3: Discriminated unions
z.discriminatedUnion() sigue funcionando pero está optimizado internamente. Si dependías de la estructura de error en fallas, los detalles pueden diferir.
Gotcha 4: .passthrough(), .strict() y .strip() están deprecated
Los schemas de objetos siguen stripeando keys desconocidas por default, pero .passthrough(), .strict() y .strip() están deprecated. Zod 4 quiere que uses .catchall() para manejar keys desconocidas de forma explícita:
// ❌ Deprecated en Zod 4 const loose = z.object({ name: z.string() }).passthrough(); const strict = z.object({ name: z.string() }).strict(); // ✅ Zod 4 recomendado const loose = z.object({ name: z.string() }).catchall(z.unknown()); const strict = z.object({ name: z.string() }).catchall(z.never());
Si usás estos métodos mucho, vas a ver deprecation warnings. Planificá la migración.
¿Deberías actualizar ahora?
Sí, si:
- Estás arrancando un proyecto nuevo (usá Zod 4 desde el día 1)
- Tus build times sufren por schemas pesados de Zod
- Deployás en edge/serverless y necesitás bundles más chicos
- Necesitás interop con JSON Schema
Esperá, si:
- Dependencias críticas todavía requieren Zod 3
- Tenés cientos de funciones
errorMapcustom - Tu equipo no tiene bandwidth para la migración
Conclusión
Zod 4 es la actualización más impactante de la validación en TypeScript desde que Zod mismo apareció. Los breaking changes son reales, pero ninguno es arbitrario. Cada uno existe para hacer a Zod más rápido, más liviano y más nativo de TypeScript.
El path de migración es claro: corré el codemod, arreglá los casos manuales, testeá, deployá. La mayoría de los proyectos pueden completar la migración en un día. Y una vez en v4, tenés acceso a @zod/mini, JSON Schema nativo, el sistema de metadatos, template literal types y errores internacionalizados: features que antes requerían pilas de dependencias terceras.
Empezá con npx @zod/codemod --transform v3-to-v4 --dry-run ./src. Fijate cuánto atrapa. El resto es grep y fix.
Explora herramientas relacionadas
Prueba estas herramientas gratuitas de Pockit