Saltar al contenido principal
Volver al blog

Zod 4: validación type-safe en TypeScript

Ray MartínRay Martín
9 min de lectura
Zod 4: validación type-safe en TypeScript

Zod 4 es una reescritura completa de la librería de validación más popular de TypeScript, ofreciendo hasta 10x más velocidad en parsing, un bundle más pequeño y nuevas APIs potentes. Si estás construyendo formularios, rutas API o cualquier frontera de datos en tu app Next.js, Zod 4 es la herramienta que mantiene tus tipos en runtime sincronizados con TypeScript — automáticamente.

¿Qué hay de nuevo en Zod 4?

Zod 4 no es una actualización incremental — es una reescritura completa centrada en rendimiento y experiencia de desarrollo:

  • 10x más rápido en parsing: Reescritura completa del motor interno de validación.
  • Bundle más pequeño: Diseño tree-shakeable, solo pagas por lo que usas.
  • z.interface(): Una nueva forma de definir schemas de objetos con mejor inferencia TypeScript.
  • z.templateLiteral(): Valida strings de template literal como `user_${number}`.
  • JSON Schema output: Método nativo .toJsonSchema() en cada schema.
  • Mejores mensajes de error: Formato rediseñado con soporte i18n.

Schemas Básicos

Si has usado Zod antes, la API core es familiar — pero todo es más rápido internamente:

typescript
import { z } from 'zod';

// Schemas primitivos
const nameSchema = z.string().min(2).max(100);
const ageSchema = z.number().int().gte(18);
const emailSchema = z.string().email();
const isActiveSchema = z.boolean();

// Schema de objeto
const UserSchema = z.object({
  name: nameSchema,
  email: emailSchema,
  age: ageSchema.optional(),
  role: z.enum(['admin', 'editor', 'viewer']),
  isActive: isActiveSchema.default(true),
});

// Tipo TypeScript inferido — siempre sincronizado
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age?: number; role: 'admin' | 'editor' | 'viewer'; isActive: boolean }

El nuevo z.interface()

Zod 4 introduce z.interface() que proporciona mejor inferencia TypeScript para tipos recursivos y complejos:

typescript
// Tipos recursivos (ej. un árbol de comentarios)
const CommentSchema = z.interface({
  id: z.number(),
  body: z.string(),
  author: z.string(),
  // Auto-referencia — z.interface() maneja esto nativamente
  replies: z.array(z.lazy(() => CommentSchema)).default([]),
});

type Comment = z.infer<typeof CommentSchema>;
// { id: number; body: string; author: string; replies: Comment[] }

Tipos Template Literal

Una de las funcionalidades más solicitadas — valida strings que siguen un patrón:

typescript
// Validar formatos de string estructurados
const userIdSchema = z.templateLiteral([z.literal('user_'), z.number()]);
// Acepta: 'user_123', 'user_0', 'user_99999'
// Rechaza: 'user_abc', 'admin_123', '123'

const hexColorSchema = z.templateLiteral([
  z.literal('#'),
  z.string().regex(/^[0-9a-fA-F]{6}$/),
]);
// Acepta: '#ff0000', '#1a2b3c'

Salida JSON Schema

Cada schema de Zod 4 puede convertirse a JSON Schema — útil para documentación de API, specs OpenAPI o definiciones de herramientas de IA:

typescript
const ProductSchema = z.object({
  name: z.string().min(1).describe('Nombre del producto'),
  price: z.number().positive().describe('Precio en céntimos'),
  category: z.enum(['electronics', 'books', 'clothing']),
});

const jsonSchema = ProductSchema.toJsonSchema();
// {
//   type: 'object',
//   properties: {
//     name: { type: 'string', minLength: 1, description: 'Nombre del producto' },
//     price: { type: 'number', exclusiveMinimum: 0, description: 'Precio en céntimos' },
//     category: { type: 'string', enum: ['electronics', 'books', 'clothing'] }
//   },
//   required: ['name', 'price', 'category']
// }

Zod 4 con React Hook Form

La integración con React Hook Form vía @hookform/resolvers funciona igual, pero ahora con el rendimiento mejorado de Zod 4:

tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const ContactSchema = z.object({
  name: z.string().min(2, 'El nombre debe tener al menos 2 caracteres'),
  email: z.string().email('Introduce un email válido'),
  message: z.string().min(10, 'El mensaje debe tener al menos 10 caracteres'),
});

type ContactForm = z.infer<typeof ContactSchema>;

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<ContactForm>({
    resolver: zodResolver(ContactSchema),
  });

  const onSubmit = (data: ContactForm) => {
    // data está totalmente tipado y validado
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} placeholder="Nombre" />
      {errors.name && <p>{errors.name.message}</p>}

      <input {...register('email')} placeholder="Email" />
      {errors.email && <p>{errors.email.message}</p>}

      <textarea {...register('message')} placeholder="Mensaje" />
      {errors.message && <p>{errors.message.message}</p>}

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

Validación en Server Actions

Usa Zod para validar datos de formulario en tus Server Actions de Next.js — un schema, validado tanto en cliente como en servidor:

typescript
// app/actions.ts
'use server';

import { z } from 'zod';

const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
  tags: z.array(z.string()).max(5),
  published: z.boolean().default(false),
});

export async function createPost(formData: FormData) {
  const raw = {
    title: formData.get('title'),
    content: formData.get('content'),
    tags: formData.getAll('tags'),
    published: formData.get('published') === 'true',
  };

  const result = CreatePostSchema.safeParse(raw);

  if (!result.success) {
    return {
      errors: result.error.flatten().fieldErrors,
    };
  }

  // result.data está totalmente tipado como CreatePost
  await db.post.create({ data: result.data });
  revalidatePath('/posts');
}

Validación en Route Handlers

Para rutas API, Zod asegura que tus request bodies siempre tengan la forma que esperas:

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const CreateUserBody = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  role: z.enum(['admin', 'user']).default('user'),
});

export async function POST(req: NextRequest) {
  const body = await req.json();
  const parsed = CreateUserBody.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json(
      { error: parsed.error.flatten() },
      { status: 400 }
    );
  }

  // parsed.data está tipado como { name: string; email: string; role: 'admin' | 'user' }
  const user = await createUser(parsed.data);
  return NextResponse.json(user, { status: 201 });
}

Migrando desde Zod 3

La migración de Zod 3 a Zod 4 es mayormente transparente, pero estos son los cambios clave:

  • z.object() sigue funcionando — z.interface() es aditivo, no un reemplazo.
  • El formato de errores ha cambiado — revisa tus manejadores de error personalizados.
  • .toJsonSchema() reemplaza el paquete zod-to-json-schema.
  • Algunos casos edge en .transform() y .pipe() se comportan diferente — testea tus pipelines.
  • El bundle es más pequeño, pero asegúrate de importar desde 'zod' (no rutas profundas).

Conclusión

Zod 4 consolida su posición como la librería esencial de validación en TypeScript. Con parsing 10x más rápido, salida nativa de JSON Schema y nuevas APIs como z.interface() y z.templateLiteral(), es la mejor forma de validar datos en cada frontera de tu aplicación Next.js — desde formularios hasta rutas API y Server Actions.

Compartir:

Artículos relacionados