Lección 2 de 312 min
Validación de datos: Joi, JSON Schema y Standard Schema
Por qué validar en el cliente y en el servidor, y tres formas de describir qué datos son válidos.
Tu formulario de registro de Angular valida el email, la longitud de la contraseña y la edad mínima. Todo perfecto… hasta que alguien envía directamente a tu API una petición con "edad": -5 y "esAdmin": true. El formulario nunca se enteró, porque esa petición no pasó por él.
En esta lección verás por qué hay que validar en los dos lados y tres formas de describir qué datos son válidos: Joi, un veterano del mundo Node.js; JSON Schema, un estándar que entienden muchos lenguajes; y Standard Schema, una interfaz común para que las herramientas acepten esquemas de cualquier librería.
Validar en el cliente y en el servidor
Validar en el cliente y en el servidor
En el cliente, para ayudar al usuario; en el servidor, para protegerte. Hacen falta las dos.
La validación en el cliente (el navegador) es una cuestión de experiencia: avisa al instante de que falta la arroba del email, sin esperar al servidor. Pero cualquiera puede saltársela: desactivando JavaScript, editando el HTML con las herramientas para desarrolladores o enviando la petición directamente con curl.
Por eso la validación en el servidor es la que protege de verdad: es la última puerta antes de tu base de datos. Todo lo que llega de fuera (el cuerpo de una petición, los parámetros de la URL, las cabeceras, los archivos, las respuestas de otras APIs) es dato no confiable hasta que lo validas, incluidos los campos que no esperabas, como un esAdmin colado. Los tipos de TypeScript no ayudan aquí: desaparecen al ejecutar.
Úsalo cuando
Siempre en los dos lados. Las reglas de formato (obligatorio, longitud, email) en ambos; las que dependen de datos del servidor, como «este email ya existe», en el servidor, aunque puedas adelantarlas en el cliente.
Ten en cuenta
Mantener las mismas reglas en dos sitios es fácil de romper. Si los dos lados usan TypeScript, comparte el esquema; si no, un JSON Schema común evita que diverjan.
# El formulario exige un email válido, pero esto llega igual al servidor
curl -X POST https://tienda.ejemplo.com/api/registro \
-H "Content-Type: application/json" \
-d '{"email": "no-es-un-email", "edad": -5, "esAdmin": true}'Qué es un esquema de validación
Una descripción de cómo deben ser los datos, que un programa comprueba por ti.
Validar a mano con if es repetitivo y siempre se escapa algún caso. Un esquema describe una sola vez la forma de los datos válidos: qué campos hay, de qué tipo son, cuáles son obligatorios y qué reglas cumplen. Un validador compara los datos con el esquema y te devuelve los datos limpios o una lista de errores con la ruta de cada uno.
Los esquemas se escriben de dos maneras: con código, encadenando funciones de una librería (Joi, Zod, Valibot, ArkType), o como datos, en un documento JSON que sigue un estándar (JSON Schema). Lo primero es más cómodo dentro de un lenguaje; lo segundo viaja entre lenguajes y herramientas.
Úsalo cuando
En cuanto validas más de un par de campos, y siempre en la frontera de tu sistema: al recibir una petición, al leer un archivo de configuración o al consumir otra API.
Ten en cuenta
Validar no es solo decir sí o no: muchos validadores también transforman los datos (recortan espacios, convierten "18" en 18, quitan campos desconocidos). Usa el valor que devuelve el validador, no los datos originales.
// A mano: es fácil olvidar un caso
if (typeof datos.email !== "string" || !datos.email.includes("@")) { /* … */ }
if (typeof datos.edad !== "number" || datos.edad < 18) { /* … */ }
// Con un esquema: una descripción y un validador que la aplica
const Registro = z.object({
email: z.email(),
edad: z.number().min(18),
});
const resultado = Registro.safeParse(datos);Nota
Muchas vulnerabilidades, como la inyección SQL, empiezan con un dato que nadie validó. Lo verás en el módulo de seguridad web.
Joi y JSON Schema
Dos enfoques muy distintos para el mismo problema: Joi describe el esquema con código JavaScript; JSON Schema, con un documento JSON que cualquier lenguaje puede leer.
Joi
Describe los datos con métodos encadenados; un veterano de la validación en Node.js.
- Origen:
- Ecosistema hapi
- Tipos de TypeScript:
- Se escriben aparte
Joi nació en el ecosistema del framework hapi y es uno de los validadores más veteranos de JavaScript. El esquema se construye encadenando métodos que se leen casi como una frase: Joi.string().email().required(). Con esquema.validate(datos) obtienes { value, error }: el valor ya limpio y, si algo falla, un error con el detalle de cada problema.
Por defecto convierte valores cuando puede (el texto "18" pasa a ser el número 18), rechaza las propiedades que el esquema no declara y se detiene en el primer error; con abortEarly: false los recoge todos. Las versiones recientes también implementan Standard Schema.
Úsalo cuando
En backends de Node.js, sobre todo con Express o hapi, para validar el cuerpo de las peticiones, los parámetros y las variables de entorno. Encontrarás mucho código existente que lo usa.
Ten en cuenta
No deduce tipos de TypeScript: tienes que escribir la interfaz aparte y mantenerla sincronizada. Y es relativamente pesado, así que rara vez se usa en el navegador.
import Joi from "joi";
const registro = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
edad: Joi.number().integer().min(18),
});
const { value, error } = registro.validate(req.body, { abortEarly: false });
if (error) {
return res.status(400).json(error.details.map((d) => d.message));
}
// value.edad ya es un número, aunque llegara como "18"JSON Schema
Un estándar para describir datos JSON con JSON, independiente del lenguaje.
- Formato:
- JSON
- Lenguajes:
- Cualquiera
JSON Schema describe la forma que debe tener un documento JSON usando otro documento JSON. Con palabras clave como type, properties, required, minLength, minimum, enum o additionalProperties dices qué campos hay, de qué tipo son y qué reglas cumplen. Conviven varias versiones de la especificación, como draft-07 y 2020-12; el campo $schema indica cuál usa cada esquema.
Su gran ventaja es que es un estándar, no una librería: el mismo esquema lo validan herramientas de JavaScript, Python, Java o Go. Por eso lo usa OpenAPI para describir APIs, como verás en el módulo de APIs, y muchos editores para autocompletar archivos de configuración.
Úsalo cuando
Cuando el contrato de datos lo comparten equipos o lenguajes distintos, o cuando el esquema tiene que guardarse, enviarse o generarse como dato: formularios dinámicos, configuración, documentación de APIs.
Ten en cuenta
JSON Schema solo describe: necesitas un validador que lo aplique, como Ajv. Y format: "email" es, por defecto, solo una anotación: muchos validadores no lo comprueban si no se lo pides.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"password": { "type": "string", "minLength": 8 },
"edad": { "type": "integer", "minimum": 18 }
},
"required": ["email", "password"],
"additionalProperties": false
}Ajv
El validador de JSON Schema más usado en JavaScript: compila el esquema a una función.
- Siglas de:
- Another JSON Schema Validator
Ajv (Another JSON Schema Validator) lee un JSON Schema y lo compila a una función de JavaScript optimizada: const validar = ajv.compile(esquema). Después, validar(datos) devuelve true o false y deja los problemas en validar.errors, con la ruta del dato (instancePath) y la regla que falló (keyword).
Compilar una vez y validar muchas lo hace muy rápido: Fastify, un framework de Node.js, lo usa por debajo para validar peticiones. Lo más habitual es activar allErrors: true, para recoger todos los errores, y añadir el paquete ajv-formats, que comprueba formatos como email o date.
Úsalo cuando
Para validar con JSON Schema en Node.js o en el navegador, por ejemplo cuando el esquema sale de un documento OpenAPI o lo comparte otro equipo.
Ten en cuenta
Ajv genera código a partir del esquema: no compiles esquemas que te envíe un usuario. Para la versión 2020-12, importa Ajv2020 desde ajv/dist/2020; la importación por defecto usa draft-07.
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";
import esquema from "./registro.schema.json";
const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
const validar = ajv.compile(esquema);
if (!validar(datos)) {
console.log(validar.errors);
// [{ instancePath: "/password", keyword: "minLength",
// message: "must NOT have fewer than 8 characters", … }]
}Standard Schema: una interfaz común
En el ecosistema de TypeScript han surgido varios validadores muy populares. Standard Schema es el acuerdo entre ellos para que las demás herramientas puedan usarlos todos sin escribir un adaptador para cada uno.
Zod, Valibot y ArkType
Validadores pensados para TypeScript: escribes el esquema y obtienes el tipo gratis.
- Tipos de TypeScript:
- Se deducen del esquema
Estas librerías siguen la idea de Joi, un esquema construido con código, pero con TypeScript en el centro: a partir del esquema deducen el tipo de los datos válidos, así que no escribes la interfaz dos veces. Si cambias el esquema, el tipo cambia con él.
Zod es la más popular; Valibot está diseñada de forma modular para que el bundler incluya solo las funciones que usas, lo que la hace muy ligera en el navegador; ArkType describe los tipos con una sintaxis parecida a la de TypeScript, en cadenas como "number > 0". Las tres funcionan en el navegador y en el servidor.
Úsalo cuando
En proyectos TypeScript que validan en el cliente y en el servidor con el mismo esquema, por ejemplo en un monorepo con código compartido.
Ten en cuenta
Estos esquemas son código: no puedes pasárselos tal cual a un equipo que trabaja en Java o Python. Si necesitas un contrato entre lenguajes, piensa en JSON Schema; algunas de estas librerías pueden exportar su esquema a ese formato.
import * as z from "zod";
const Registro = z.object({
email: z.email(),
edad: z.number().int().min(18),
});
// El tipo se deduce del esquema: { email: string; edad: number }
type Registro = z.infer<typeof Registro>;
const resultado = Registro.safeParse(datos);
if (!resultado.success) console.log(resultado.error.issues);Standard Schema
Una interfaz común para que una herramienta acepte esquemas de cualquier librería compatible.
- Propiedad:
- ~standard
- Tipos:
- @standard-schema/spec
Si escribes una librería que recibe esquemas (un router, un gestor de formularios, un cliente de API), ¿con qué validador te casas? Antes había que elegir uno o escribir un adaptador para cada uno. Standard Schema es una interfaz de TypeScript, acordada por los autores de Zod, Valibot y ArkType, que lo resuelve: cada esquema compatible expone una propiedad ~standard con el mismo formato.
Esa propiedad contiene version, vendor (el nombre de la librería) y una función validate(valor) que devuelve { value } si todo va bien o { issues } con la lista de problemas, cada uno con su message y su path. La implementan Zod, Valibot, ArkType y las versiones recientes de Joi, y la aceptan herramientas como tRPC, TanStack Form o Signal Forms de Angular.
El ~ del nombre tiene un motivo: evita choques con propiedades que ya existían en las librerías y hace que el editor muestre esa clave al final del autocompletado.
Úsalo cuando
Al escribir código que debe aceptar «cualquier validador», y al elegir librería: si implementa Standard Schema, podrás usarla con más herramientas sin adaptadores.
Ten en cuenta
Standard Schema no valida nada por sí mismo ni dice cómo se escribe un esquema: solo cómo se ejecuta y qué devuelve. Tampoco sirve para guardar o enviar esquemas entre lenguajes; para eso sigue estando JSON Schema.
import type { StandardSchemaV1 } from "@standard-schema/spec";
// Tu función acepta cualquier esquema compatible
export async function validar<T extends StandardSchemaV1>(
esquema: T,
datos: unknown,
): Promise<StandardSchemaV1.InferOutput<T>> {
const resultado = await esquema["~standard"].validate(datos);
if (resultado.issues) {
throw new Error(resultado.issues.map((i) => i.message).join("; "));
}
return resultado.value;
}
// Quien la usa elige su librería
await validar(z.object({ nombre: z.string() }), cuerpo); // Zod
await validar(type({ nombre: "string" }), cuerpo); // ArkType¿Sabías que…?
El paquete @standard-schema/spec no contiene código que se ejecute: solo tipos de TypeScript. Una librería puede incluso copiar la interfaz en su código sin añadir ninguna dependencia.
Interactivo
Cuatro piezas para validar datos
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Joi | JSON Schema y Ajv | Zod, Valibot y ArkType |
|---|---|---|---|
| Qué es | Una librería de validación | Un estándar y su validador más usado en JavaScript | Librerías de validación |
| Cómo se escribe el esquema | Código: métodos encadenados | Datos: un documento JSON | Código pensado para TypeScript |
| Tipos de TypeScript | Se escriben aparte | Con utilidades como JSONSchemaType de Ajv | Se deducen del esquema |
| Entre lenguajes | No: solo JavaScript | Sí | No, aunque algunas exportan a JSON Schema |
| Dónde brilla | Backends de Node.js | Contratos compartidos, OpenAPI, configuración | Proyectos TypeScript de cliente y servidor |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Mantienes un backend en Node.js con Express escrito en JavaScript, sin TypeScript, y quieres validar el cuerpo de cada petición con mensajes de error detallados.
Para recordar
- Valida en el cliente para ayudar al usuario y en el servidor para protegerte: el cliente siempre se puede saltar.
- Un esquema describe una vez los datos válidos; usa el valor que devuelve el validador, ya limpio y convertido.
- Joi, Zod, Valibot y ArkType describen esquemas con código; las tres últimas deducen además los tipos de TypeScript.
- JSON Schema es un estándar en JSON que entienden muchos lenguajes; en JavaScript se valida sobre todo con Ajv.
- Standard Schema no valida: es la interfaz
~standardque permite a una herramienta aceptar esquemas de cualquier librería compatible.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Por qué no basta con validar en el navegador?
2.Con Joi, el esquema
Joi.number()recibe el texto"18"con las opciones por defecto. ¿Qué ocurre?3.¿Qué ventaja principal tiene JSON Schema frente a Joi?
4.¿Qué hace
ajv.compile(esquema)?5.¿Qué es Standard Schema?