Zod v4 em Lançamento - A Evolução da Validação Type-Safe em TypeScript

2025.12.02

Visão Geral do Zod v4

Zod v4 é a próxima versão principal da biblioteca de validação de schemas para TypeScript. Inclui melhorias de performance, melhores mensagens de erro e novos utilitários.

Melhorias de Performance

ItemZod v3Zod v4Melhoria
Velocidade de parsing de objeto10.2μs6.8μs33% mais rápido
Velocidade de parsing de array (1000 elementos)850μs520μs39% mais rápido
Tamanho do bundle (gzip)14KB10KB29% menor

Novas Funcionalidades

  • Mensagens de erro melhoradas
  • Saída JSON Schema aprimorada
  • API de metadados
  • Novos tipos primitivos
  • Simplificação de validação personalizada

Uso Básico

import { z } from 'zod';

// Definição de schema
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(2).max(100),
  age: z.number().int().positive().optional(),
  role: z.enum(['admin', 'user', 'guest']),
  preferences: z.object({
    theme: z.enum(['light', 'dark']).default('light'),
    notifications: z.boolean().default(true),
  }).optional(),
  createdAt: z.coerce.date(),
});

// Inferência de tipo
type User = z.infer<typeof UserSchema>;
// {
//   id: string;
//   email: string;
//   name: string;
//   age?: number;
//   role: 'admin' | 'user' | 'guest';
//   preferences?: { theme: 'light' | 'dark'; notifications: boolean };
//   createdAt: Date;
// }

// Validação
const result = UserSchema.safeParse(input);
if (result.success) {
  console.log(result.data); // Type-safe
} else {
  console.log(result.error.issues);
}

Mensagens de Erro Melhoradas

// Zod v4: Caminhos de erro mais detalhados
const OrderSchema = z.object({
  items: z.array(
    z.object({
      productId: z.string().uuid(),
      quantity: z.number().int().positive(),
    })
  ).min(1),
});

const result = OrderSchema.safeParse({
  items: [
    { productId: 'invalid-uuid', quantity: -1 },
  ],
});

// Saída de erro
result.error?.issues.forEach((issue) => {
  console.log({
    path: issue.path.join('.'),  // "items.0.productId"
    message: issue.message,       // "Invalid uuid"
    code: issue.code,             // "invalid_string"
  });
});

Mensagens de Erro Personalizadas

const FormSchema = z.object({
  email: z.string({
    required_error: 'O endereço de email é obrigatório',
    invalid_type_error: 'O endereço de email deve ser uma string',
  }).email({
    message: 'Por favor, insira um endereço de email válido',
  }),

  password: z.string()
    .min(8, { message: 'A senha deve ter pelo menos 8 caracteres' })
    .regex(/[A-Z]/, { message: 'Inclua pelo menos uma letra maiúscula' })
    .regex(/[0-9]/, { message: 'Inclua pelo menos um número' }),

  age: z.number({
    required_error: 'Por favor, insira a idade',
    invalid_type_error: 'A idade deve ser um número',
  }).int().min(0).max(150),
});

// Mapa de erros para internacionalização
const errorMap: z.ZodErrorMap = (issue, ctx) => {
  if (issue.code === z.ZodIssueCode.too_small) {
    if (issue.type === 'string') {
      return { message: `Por favor, insira pelo menos ${issue.minimum} caracteres` };
    }
    if (issue.type === 'number') {
      return { message: `Por favor, insira um valor de pelo menos ${issue.minimum}` };
    }
  }
  return { message: ctx.defaultError };
};

z.setErrorMap(errorMap);

Novos Tipos Utilitários

z.coerce (Coerção de Tipo)

// Converter string para número
const NumberSchema = z.coerce.number();
NumberSchema.parse('42');     // 42
NumberSchema.parse('3.14');   // 3.14
NumberSchema.parse(true);     // 1

// Converter string para data
const DateSchema = z.coerce.date();
DateSchema.parse('2024-01-01');           // Date
DateSchema.parse(1704067200000);          // Date
DateSchema.parse(new Date());             // Date

// Converter string para booleano
const BoolSchema = z.coerce.boolean();
BoolSchema.parse('true');     // true
BoolSchema.parse('false');    // false
BoolSchema.parse(1);          // true
BoolSchema.parse(0);          // false

z.pipe (Cadeia de Transformação)

// String → Número → Validação
const PriceSchema = z
  .string()
  .pipe(z.coerce.number().positive());

PriceSchema.parse('100');   // 100
PriceSchema.parse('-50');   // Error: Number must be positive

// Parsing de string JSON
const JsonDataSchema = z
  .string()
  .transform((str) => JSON.parse(str))
  .pipe(z.object({
    name: z.string(),
    value: z.number(),
  }));

JsonDataSchema.parse('{"name": "test", "value": 42}');

z.discriminatedUnion (Union Discriminada)

// Discriminação de tipo de evento
const EventSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('user_created'),
    userId: z.string(),
    email: z.string().email(),
  }),
  z.object({
    type: z.literal('user_updated'),
    userId: z.string(),
    changes: z.record(z.unknown()),
  }),
  z.object({
    type: z.literal('user_deleted'),
    userId: z.string(),
    deletedAt: z.date(),
  }),
]);

type Event = z.infer<typeof EventSchema>;
// Discriminável em TypeScript
function handleEvent(event: Event) {
  switch (event.type) {
    case 'user_created':
      console.log(event.email); // Type-safe
      break;
    case 'user_updated':
      console.log(event.changes);
      break;
    case 'user_deleted':
      console.log(event.deletedAt);
      break;
  }
}

Saída JSON Schema

import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

const ProductSchema = z.object({
  id: z.string().uuid().describe('Product unique identifier'),
  name: z.string().min(1).max(255).describe('Product name'),
  price: z.number().positive().describe('Price in cents'),
  category: z.enum(['electronics', 'clothing', 'food']),
  tags: z.array(z.string()).optional(),
  metadata: z.record(z.unknown()).optional(),
});

const jsonSchema = zodToJsonSchema(ProductSchema, {
  name: 'Product',
  target: 'openApi3',
});

// Saída:
// {
//   "$schema": "http://json-schema.org/draft-07/schema#",
//   "type": "object",
//   "properties": {
//     "id": {
//       "type": "string",
//       "format": "uuid",
//       "description": "Product unique identifier"
//     },
//     "name": {
//       "type": "string",
//       "minLength": 1,
//       "maxLength": 255,
//       "description": "Product name"
//     },
//     ...
//   },
//   "required": ["id", "name", "price", "category"]
// }

API de Metadados

// Adicionar metadados ao schema
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string(),
}).meta({
  title: 'User',
  description: 'Represents a user in the system',
  examples: [
    { id: '123e4567-e89b-12d3-a456-426614174000', email: 'user@example.com', name: 'John Doe' },
  ],
});

// Obter metadados
const metadata = UserSchema._def.meta;
console.log(metadata.title);       // 'User'
console.log(metadata.description); // 'Represents a user in the system'

// Utilizar para geração OpenAPI
function generateOpenApiSchema(schema: z.ZodType) {
  const meta = schema._def.meta ?? {};
  return {
    title: meta.title,
    description: meta.description,
    examples: meta.examples,
    // ...
  };
}

Branded Types

// Distinção em nível de tipo
const UserId = z.string().uuid().brand<'UserId'>();
const PostId = z.string().uuid().brand<'PostId'>();

type UserId = z.infer<typeof UserId>;
type PostId = z.infer<typeof PostId>;

// Exemplo de uso
function getUser(id: UserId): Promise<User> { ... }
function getPost(id: PostId): Promise<Post> { ... }

const userId = UserId.parse('123e4567-e89b-12d3-a456-426614174000');
const postId = PostId.parse('987fcdeb-51a2-34d6-b789-123456789abc');

getUser(userId);  // OK
getUser(postId);  // Erro TypeScript!

Validação de Formulário

// Integração com React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const SignUpSchema = z.object({
  email: z.string().email('Por favor, insira um endereço de email válido'),
  password: z.string().min(8, 'Por favor, insira pelo menos 8 caracteres'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'As senhas não coincidem',
  path: ['confirmPassword'],
});

type SignUpForm = z.infer<typeof SignUpSchema>;

function SignUpPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SignUpForm>({
    resolver: zodResolver(SignUpSchema),
  });

  const onSubmit = (data: SignUpForm) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="password" {...register('password')} />
      {errors.password && <span>{errors.password.message}</span>}

      <input type="password" {...register('confirmPassword')} />
      {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}

      <button type="submit">Sign Up</button>
    </form>
  );
}

Padrões Avançados

Schema Recursivo

// Estrutura de árvore
interface Category {
  id: string;
  name: string;
  children: Category[];
}

const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    id: z.string(),
    name: z.string(),
    children: z.array(CategorySchema),
  })
);

Validação Condicional

const PaymentSchema = z.discriminatedUnion('method', [
  z.object({
    method: z.literal('credit_card'),
    cardNumber: z.string().regex(/^\d{16}$/),
    expiryDate: z.string().regex(/^\d{2}\/\d{2}$/),
    cvv: z.string().regex(/^\d{3,4}$/),
  }),
  z.object({
    method: z.literal('bank_transfer'),
    bankCode: z.string(),
    accountNumber: z.string(),
  }),
  z.object({
    method: z.literal('paypal'),
    email: z.string().email(),
  }),
]);
← Voltar para a lista