Back

Migrando pro Zod 4: Guia completo de breaking changes, ganhos de performance e novas funcionalidades

Se você escreveu TypeScript nos últimos três anos, usou Zod. Tá nas suas API routes, na validação de formulários, nos endpoints de tRPC, no parser de variáveis de ambiente. Zod tá em todo lugar, e o Zod 4 acabou de mudar quase tudo.

Os números impressionam: parsing de strings 14x mais rápido, arrays 7x mais rápido, bundle core 2.3x menor, e compilação TypeScript até 10x mais rápida. Mas esses ganhos vêm com uma lista gigante de breaking changes que vão pintar seu CI de vermelho no segundo que você der bump na versão.

Este guia cobre cada breaking change, mostra o código before/after exato, e te dá uma estratégia de migração pra sua equipe não ficar uma semana debugando erros de schema.

Por que mudou tanto

O Zod 3 foi projetado quando o sistema de tipos do TypeScript era menos maduro e o tamanho do bundle não era uma preocupação central pra bibliotecas de validação server-side. Conforme o ecossistema evoluiu (funções serverless com cold starts, Edge runtimes com limites de tamanho rígidos, monorepos com milhares de schemas), a arquitetura do Zod 3 começou a mostrar seus limites.

O Zod 4 é uma reescrita do zero que ataca três problemas fundamentais:

  1. Tamanho do bundle: A API de method-chaining do Zod 3 tornava o tree-shaking praticamente impossível. Um único import arrastava a biblioteca inteira.
  2. Performance de parsing: O pipeline de validação tinha overhead desnecessário com criação de objetos e lookups na cadeia de protótipos.
  3. Velocidade de compilação TypeScript: Schemas complexos do Zod geravam uma quantidade absurda de type instantiations, fazendo o tsc rastejar em codebases grandes.

Pra resolver isso de verdade, precisava quebrar a API. Bora ver uma por uma.

Breaking Change #1: Parâmetro de erro unificado

Essa é a mudança que vai quebrar mais código. O Zod 3 tinha três formas diferentes de customizar mensagens de erro:

// ❌ Zod 3 — Três parâmetros diferentes const schema = z.string({ required_error: "O nome é obrigatório", invalid_type_error: "O nome deve ser uma string", }); const email = z.string().email({ message: "Email inválido" }); const age = z.number({ errorMap: (issue, ctx) => { if (issue.code === "too_small") return { message: "Deve ter 18 anos ou mais" }; return { message: ctx.defaultError }; }, });

O Zod 4 substitui tudo por um único parâmetro error:

// ✅ Zod 4 — Parâmetro error unificado const schema = z.string({ error: "O nome é obrigatório", }); const email = z.string().email({ error: "Email inválido", }); const age = z.number({ error: (issue) => { if (issue.code === "too_small") return "Deve ter 18 anos ou mais"; return "Idade inválida"; }, });

A propriedade message agora está deprecated em todos os métodos. required_error, invalid_type_error e errorMap sumiram completamente. Use o parâmetro unificado error em tudo. O codemod oficial cuida da maioria:

npx @zod/codemod --transform v3-to-v4 ./src

Os errorMap customizados precisam de revisão manual porque a assinatura mudou de (issue, ctx) => { message: string } pra (issue) => string.

Breaking Change #2: Validadores de formato top-level

O Zod 3 usava method chaining pra validação de formatos de string. O Zod 4 promove os mais comuns pra funções top-level:

// ❌ Zod 3 — Method chaining const emailSchema = z.string().email(); const uuidSchema = z.string().uuid(); const urlSchema = z.string().url(); // ✅ Zod 4 — Funções top-level const emailSchema = z.email(); const uuidSchema = z.uuid(); const urlSchema = z.url();

z.string().email() puxa a classe ZodString inteira mesmo que você só precise de validação de email. Funções top-level permitem tree-shaking de verdade.

Importante: As versões com method-chain (z.string().email()) ainda funcionam mas estão oficialmente deprecated no Zod 4. Não vão ser removidas imediatamente, então dá pra migrar gradualmente, mas espere que sejam removidas numa futura versão major.

Também: z.string().ip() e z.string().cidr() foram completamente removidos, substituídos por z.ipv4(), z.ipv6(), z.cidrv4() e z.cidrv6(). E z.uuid() agora é mais estrito, validando variant bits da RFC 9562/4122. Se precisar de um padrão mais permissivo, use o novo z.guid().

Breaking Change #3: Tipos de input de Coercion agora são unknown

O namespace z.coerce continua existindo no Zod 4, mas o tipo de input de todos os schemas coercionados mudou do tipo específico pra unknown:

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

Isso reflete melhor o que a coerção realmente faz: aceita qualquer coisa e tenta converter. Mas significa que o TypeScript não vai mais narrowar o tipo de input, o que pode gerar erros de tipo em código que dependia do tipo narrowado.

O namespace z.coerce em si (z.coerce.number(), z.coerce.string(), etc.) continua funcionando como antes: a superfície da API não mudou, só o tipo de input inferido.

Breaking Change #4: Comportamento de Optional + Default mudou

Sutil mas perigoso. No Zod 3, chamar .optional() num schema com .default() ou .catch() ignorava propriedades ausentes. No Zod 4, o valor default é sempre aplicado:

const schema = z.object({ theme: z.string().default("light").optional(), }); // Zod 3: { theme: undefined } → { theme: undefined } ← Propriedade ausente ignorada // Zod 4: { theme: undefined } → { theme: "light" } ← Default aplicado

O comportamento ficou mais previsível, mas pode quebrar código que checa undefined pra detectar "não fornecido".

Outra mudança no .default(): o valor default agora deve coincidir com o tipo de output, não de input. No Zod 3, .default() parseava o valor default pelo schema. No Zod 4, faz short-circuit e retorna o default direto:

// Zod 3: default coincidia com tipo INPUT, era parseado const schema = z.string() .transform(val => val.length) .default("tuna"); // string input → parsed → 4 schema.parse(undefined); // => 4 // Zod 4: default coincide com tipo OUTPUT, retornado direto const schema = z.string() .transform(val => val.length) .default(0); // number output, retornado direto schema.parse(undefined); // => 0

Pra replicar o comportamento antigo ("pre-parse default"), o Zod 4 introduz .prefault():

// ✅ Zod 4 — .prefault() pro comportamento antigo de .default() const schema = z.string() .transform(val => val.length) .prefault("tuna"); schema.parse(undefined); // => 4

Breaking Change #5: TypeScript Strict mode obrigatório

O Zod 4 exige strict: true no tsconfig.json e TypeScript 5.5+:

{ "compilerOptions": { "strict": true, "target": "ES2022", "module": "ESNext" } }

Nova funcionalidade: @zod/mini

Se você deploya em Edge runtimes ou funções serverless onde o tamanho do bundle faz diferença, isso aqui é game changer:

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"]), });
Funcionalidadezod@zod/mini
Bundle core~13KB gzip~5.5KB gzip
.transform()
.pipe()
JSON Schema

Pra API route handlers que só validam sem transformar, @zod/mini é a jogada certa.

Nova funcionalidade: Conversão JSON Schema nativa

Não precisa mais instalar zod-to-json-schema. O Zod 4 traz geração 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();

A direção inversa também funciona:

const schema = z.fromJSONSchema({ type: "object", properties: { name: { type: "string" }, age: { type: "integer", minimum: 0 }, }, required: ["name"], });

Muito útil pra interop com specs OpenAPI, geradores de form baseados em JSON Schema e definições de AI tools (MCP, function calling).

Nova funcionalidade: Metadados de schema

Agora dá pra anexar metadados fortemente tipados nos seus schemas:

const NameSchema = z.string().min(1).max(100).meta({ label: "Nome completo", placeholder: "João Silva", helpText: "Insira seu nome legal.", }); const meta = NameSchema.meta();

Isso habilita padrões como geração de formulários baseada em schemas. Os metadados são preservados através de operações como .optional(), .array() e .transform().

Nova funcionalidade: Erros internacionalizados

O Zod 4 vem com um sistema de locales pra traduzir mensagens de validação:

import { z } from "zod"; import { pt } from "@zod/locales/pt"; z.config({ locale: pt }); const result = z.string().min(5).safeParse("oi"); // result.error.issues[0].message → "Deve ter pelo menos 5 caracteres"

Acabou a era de envolver cada schema com error maps customizados só pra suportar múltiplos idiomas.

Nova funcionalidade: Template Literal Types

O Zod 4 introduz z.templateLiteral() pra validar strings que seguem um padrão 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}`

Muito útil pra validar formatos de string estruturados como valores CSS, strings de versão semântica ou padrões de endpoints de API, com inferência de tipos TypeScript completa.

Benchmarks de performance

OperaçãoZod 3Zod 4Melhoria
Parse de strings1.0x14x1,300%
Parse de arrays1.0x7x600%
Parse de objetos1.0x6.5x550%
Tamanho do bundle~31KB gzip~13KB gzip2.3x menor
TS type instantiations1.0xaté 10x menos900%

A melhoria na compilação TypeScript é particularmente impactante. Em codebases com centenas de schemas, tem benchmark mostrando o tsc caindo de 47 segundos pra 5 segundos.

Estratégia de migração: o caminho seguro

Não faça migração big-bang. Vai por fases:

Fase 1: Preparação

  1. Conferir se o TypeScript strict mode tá habilitado
  2. Rodar o codemod em dry-run:
npx @zod/codemod --transform v3-to-v4 --dry-run ./src
  1. Procurar errorMap no codebase pra identificar migrações manuais
  2. Checar padrões .optional().default() (comportamento diferente no v4)
  3. Confirmar TypeScript 5.5+

Fase 2: Executar o codemod

npx @zod/codemod --transform v3-to-v4 ./src git diff --stat

Fase 3: Correções manuais

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: Testar

  1. Formato de mensagens de erro: Testes que fazem assert em mensagens específicas vão quebrar
  2. Tipos de input de coerção: Tipos de input de z.coerce.* mudaram pra unknown, o que pode gerar novos erros de tipo
  3. Optional+Default: Checks de undefined em campos opcionais com default vão mudar

Fase 5: Adotar novas features (opcional)

  • Substituir zod-to-json-schema por .toJSONSchema()
  • Gerenciar metadados de schemas com z.registry()
  • Migrar schemas de só validação pra @zod/mini
  • Usar z.templateLiteral() pra validação de strings estruturados

Armadilhas comuns

Armadilha 1: Peer dependencies

Bibliotecas que dependem do Zod 3 podem não aceitar Zod 4 como peer dependency:

npm ls zod

Em março de 2026, a maioria das bibliotecas principais já suporta Zod 4: tRPC v11+, @tanstack/react-form, react-hook-form v8 + @hookform/resolvers v4+.

Armadilha 2: z.input / z.output mudaram

z.infer<typeof schema> continua igual. Mas se você tava usando z.input ou z.output, os tipos relacionados a defaults e transforms podem ser diferentes no v4.

Armadilha 3: Discriminated unions

z.discriminatedUnion() continua funcionando mas foi otimizado internamente. Se você dependia da estrutura de erro nas falhas, os detalhes podem ser diferentes.

Armadilha 4: .passthrough(), .strict() e .strip() estão deprecated

Schemas de objeto continuam stripando keys desconhecidas por default, mas .passthrough(), .strict() e .strip() estão todos deprecated. O Zod 4 quer que você use .catchall() pra lidar com keys desconhecidas de forma explícita:

// ❌ Deprecated no 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());

Se você usa esses métodos bastante, vai ver deprecation warnings. Planeje a migração.

Devo atualizar agora?

Sim, se:

  • Tá começando um projeto novo (use Zod 4 desde o dia 1)
  • Seus build times tão sofrendo por schemas pesados de Zod
  • Deploya em edge/serverless e precisa de bundles menores
  • Precisa de interop com JSON Schema

Pode esperar, se:

  • Dependências críticas ainda exigem Zod 3
  • Tem centenas de funções errorMap customizadas
  • Sua equipe não tem bandwidth pra migração agora

Conclusão

O Zod 4 é a atualização mais impactante da validação em TypeScript desde que o Zod apareceu. Os breaking changes são reais, mas nenhum é arbitrário. Cada um existe pra fazer o Zod mais rápido, mais leve e mais TypeScript-native.

O caminho de migração é direto: rode o codemod, corrija os casos manuais, teste, faça deploy. A maioria dos projetos consegue completar a migração em um dia. E uma vez no v4, você tem acesso a @zod/mini, JSON Schema nativo, sistema de metadados, template literal types e erros internacionalizados, tudo sem dependências de terceiros.

Comece com npx @zod/codemod --transform v3-to-v4 --dry-run ./src. Veja quanto ele pega. O resto é grep e fix.

ZodTypeScriptvalidationmigrationschemaJavaScriptNode.jsdeveloper tools

Explore ferramentas relacionadas

Experimente estas ferramentas gratuitas do Pockit