Zod x Yup: O Que É Isso? Qual É Melhor?

Um comparativo prático entre as duas bibliotecas de validação de esquema mais usadas do mercado, com foco em quem trabalha com TypeScript.


Esse não é um comparativo genérico saindo listando várias bibliotecas de validação. É um comparativo direto entre duas: Zod e Yup, com exemplos práticos em código, focado principalmente em quem trabalha com TypeScript.

E já vou adiantar, sem empatar o jogo, se você usa TypeScript, o Zod é a escolha melhor na grande maioria dos casos. Vou mostrar o porquê com exemplos reais.

O que são essas bibliotecas

Zod e Yup são bibliotecas de validação de esquema. Esquema é basicamente um protótipo que descreve o formato que um dado deveria ter. Você cria esse protótipo de um lado e depois compara um dado real com ele para ver se está no padrão esperado.

Muita gente associa esse tipo de biblioteca só com validação de formulário, ou só com validação de retorno de API. Mas serve para qualquer dado, venha ele de onde vier: retorno de função, corpo de requisição, resposta de API, dado que você vai enviar para uma API. Se você tem um formato esperado e quer validar se o dado real segue esse formato, é para isso que essas bibliotecas existem.

Um pouco de contexto

O Yup é bem mais antigo. Ele nasceu em JavaScript, antes do TypeScript existir, e por isso é a biblioteca usada pela maioria dos sistemas mais antigos em produção. Hoje ele ainda tem mais downloads semanais que o Zod, algo em torno de 4.4 milhões contra 3.6 milhões. Só que o gráfico de crescimento conta outra história: o Zod está em uma curva ascendente forte, enquanto o Yup está estável.

Quando o TypeScript ganhou força, o time do Yup adicionou suporte a tipos na biblioteca já existente. Só que essa adaptação trouxe alguns problemas de integração, porque o Yup não foi pensado desde o início para gerar tipos. O criador do Zod, insatisfeito com essas inconsistências, criou uma biblioteca do zero, já 100% em TypeScript, justamente para resolver esses problemas.

Objetos obrigatórios por padrão: direções opostas

Essa é a primeira diferença que já pega muita gente de surpresa.

No Yup, todo campo criado no esquema é opcional por padrão:

import * as yup from "yup"
 
const schema = yup.object({
  name: yup.string(),
})
 
const data = {}
 
console.log(schema.validateSync(data))
// funciona, retorna {} sem erro

No Zod, é o contrário. Todo campo é obrigatório por padrão:

import { z } from "zod"
 
const schema = z.object({
  name: z.string(),
})
 
const data = {}
 
schema.parse(data)
// dá erro, porque "name" é obrigatório

Para deixar um campo opcional no Zod, você marca explicitamente com .optional(). Para tornar um campo obrigatório no Yup, você marca com .required(). Nenhuma das duas abordagens está errada, são só filosofias diferentes. O problema real aparece quando você entra na inferência de tipos.

Inferência de tipos: onde o Yup historicamente errou

Inferência de tipo é o recurso que permite criar um type do TypeScript automaticamente a partir do esquema, sem precisar escrever a estrutura duas vezes.

No Zod:

type User = z.infer<typeof schema>

No Yup:

type User = yup.InferType<typeof schema>

O problema apontado pelo criador do Zod é que, na época, o Yup gerava a inferência de tipo errada. Como o Yup considera os campos opcionais por padrão no funcionamento real, o tipo inferido deveria refletir isso, mas ele gerava o campo como obrigatório na tipagem. Ou seja, a tipagem dizia uma coisa e o comportamento real fazia outra. Esse tipo de inconsistência é justamente o que uma biblioteca de validação não pode ter, porque o ganho de usar TypeScript é confiar no tipo que você vê.

Validação de e-mail: os dois resolvem bem

Para casos simples, os dois funcionam de forma parecida.

Yup:

const schema = yup.object({
  email: yup.string().email("E-mail inválido"),
})

Zod:

const schema = z.object({
  email: z.string().email("E-mail inválido"),
})

Ambos validam se a string tem o formato de e-mail e aceitam mensagem de erro customizada. Até aqui, empate técnico.

Union types: onde o Yup mostra a maior fragilidade

Aqui está o problema mais grave encontrado na prática. Union type é quando um campo pode ter um valor entre um conjunto fechado de opções, por exemplo, "masculino" | "feminino".

No Zod, isso é direto:

const schema = z.object({
  name: z.string(),
  sexo: z.literal("masculino").or(z.literal("feminino")),
})

O tipo inferido fica exatamente "masculino" | "feminino", e a validação em tempo de execução respeita esse tipo. Se você mandar qualquer outro valor, o parse falha.

No Yup, a forma mais próxima disso é usar .mixed():

const schema = yup.object({
  name: yup.string(),
  sexo: yup.mixed<"masculino" | "feminino">(),
})

O tipo inferido até fica correto na tipagem. O problema é que, na prática, o .mixed() sozinho não impede que qualquer outro valor passe na validação. Se você mandar um valor fora dessas duas opções, o Yup aceita mesmo assim, o que é uma inconsistência grave entre o tipo gerado e o comportamento real de validação.

Para forçar a restrição de verdade, é preciso adicionar .oneOf() também:

const schema = yup.object({
  name: yup.string(),
  sexo: yup.mixed<"masculino" | "feminino">().oneOf(["masculino", "feminino"]),
})

Ou seja, no Yup você precisa repetir a lista de valores duas vezes, uma para gerar o tipo e outra para forçar a validação de fato. No Zod, uma única definição já resolve as duas coisas.

Funções e promises: recurso que o Yup simplesmente não tem

O Zod permite criar esquema para validar a assinatura de uma função, incluindo parâmetros e retorno:

const addSchema = z.function({
  input: [z.number(), z.number()],
  output: z.number(),
})

E também permite tipar promises:

const schema = z.object({
  fullName: z.promise(z.string()),
})

O Yup não tem suporte nativo para nenhum dos dois casos. Se o seu projeto usa esquemas para validar funções ou valores assíncronos, essa já é uma limitação direta.

Quando usar cada um

Se você trabalha com TypeScript, o Zod tende a ser a escolha mais segura. A tipagem gerada é confiável, o comportamento em tempo de execução acompanha o tipo, e o suporte a casos mais avançados, como union types, funções e promises, é nativo e sem gambiarra.

O Yup ainda é uma biblioteca extremamente usada no mercado, principalmente em projetos mais antigos ou em JavaScript puro, sem tipagem. Se o seu projeto já usa Yup e não tem esses problemas específicos, não existe motivo urgente para trocar.

O resumo

Yup nasceu em JavaScript e ganhou suporte a TypeScript depois, o que trouxe inconsistências entre o tipo gerado e o comportamento real de validação, principalmente em union types. Zod nasceu já pensado para TypeScript e resolve esses casos de forma nativa e consistente, incluindo suporte a funções e promises que o Yup não tem. Para quem trabalha com TypeScript, o Zod é a opção mais confiável hoje.