JSON Schema: Valida Tus APIs Antes de Desplegarlas
Aprende a usar JSON Schema para validar datos de entrada en APIs REST, generar documentación automática y prevenir errores en producción antes de que ocurran.
El bug que llegó a producción un viernes por la tarde
Imagina este escenario: un frontend envía una petición a tu API con un campo age como string ("25") en lugar de number (25). Tu código hace if (user.age >= 18) y en JavaScript, "25" >= 18 es true gracias a la coerción de tipos. Todo parece funcionar. Hasta que alguien envía age: "adulto" y "adulto" >= 18 es false, rechazando a un usuario legítimo.
Este tipo de bug es exactamente lo que JSON Schema previene.
¿Qué es JSON Schema?
JSON Schema es un vocabulario estándar (especificado en IETF RFC) que permite describir y validar la estructura de documentos JSON. En lugar de validar manualmente cada campo, defines una vez cómo debe ser tu JSON y dejas que una librería haga el trabajo.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1, "maxLength": 100 },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"email": { "type": "string", "format": "email" }
},
"required": ["name", "email"]
}
Con este schema puedes validar cualquier objeto JSON: si no cumple las reglas, obtienes errores descriptivos; si cumple, sabes que los datos son estructuralmente correctos.
Puedes experimentar con JSON en nuestra herramienta Formateador JSON y Validador JSON mientras lees este tutorial.
Los tipos básicos
JSON Schema soporta los mismos tipos que JSON:
{ "type": "string" } // cadena de texto
{ "type": "number" } // número (entero o decimal)
{ "type": "integer" } // solo enteros
{ "type": "boolean" } // true o false
{ "type": "null" } // null
{ "type": "array" } // array
{ "type": "object" } // objeto
Puedes permitir múltiples tipos:
{ "type": ["string", "null"] } // string o null (campo opcional que puede ser null)
Validaciones por tipo
Strings
{
"type": "string",
"minLength": 2,
"maxLength": 50,
"pattern": "^[a-zA-Z]+$",
"format": "email"
}
Formatos estándar disponibles: "email", "uri", "date", "time", "date-time", "uuid", "ipv4", "ipv6".
Numbers
{
"type": "number",
"minimum": 0,
"maximum": 100,
"exclusiveMinimum": 0, // mayor que 0 (no incluye el 0)
"multipleOf": 0.5 // solo múltiplos de 0.5
}
Arrays
{
"type": "array",
"items": { "type": "string" }, // todos los items deben ser strings
"minItems": 1,
"maxItems": 10,
"uniqueItems": true // no puede haber duplicados
}
Objects
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
},
"required": ["id", "name"], // campos obligatorios
"additionalProperties": false // rechaza campos no definidos
}
Un schema real: endpoint de creación de usuario
Así quedaría el schema completo para un endpoint POST /users:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://api.ejemplo.com/schemas/create-user.json",
"title": "CreateUserRequest",
"description": "Cuerpo de la petición para crear un nuevo usuario",
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 2,
"maxLength": 100,
"description": "Nombre completo del usuario"
},
"email": {
"type": "string",
"format": "email",
"description": "Email único del usuario"
},
"age": {
"type": "integer",
"minimum": 18,
"maximum": 120,
"description": "Edad en años (debe ser mayor de edad)"
},
"role": {
"type": "string",
"enum": ["admin", "editor", "viewer"],
"default": "viewer",
"description": "Rol del usuario en el sistema"
},
"tags": {
"type": "array",
"items": { "type": "string", "maxLength": 30 },
"maxItems": 10,
"uniqueItems": true
}
},
"required": ["name", "email"],
"additionalProperties": false
}
Validación en Node.js con AJV
AJV (Another JSON Validator) es la librería de validación JSON Schema más rápida y usada en el ecosistema JavaScript.
npm install ajv ajv-formats
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const ajv = new Ajv({ allErrors: true }); // allErrors: muestra todos los errores, no solo el primero
addFormats(ajv); // añade soporte para "email", "date", "uri", etc.
const createUserSchema = {
type: 'object',
properties: {
name: { type: 'string', minLength: 2, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 18 },
role: { type: 'string', enum: ['admin', 'editor', 'viewer'] },
},
required: ['name', 'email'],
additionalProperties: false,
};
const validateUser = ajv.compile(createUserSchema);
// Uso en tu handler de Express/Fastify/etc.
function createUserHandler(req, res) {
const valid = validateUser(req.body);
if (!valid) {
return res.status(400).json({
error: 'Datos inválidos',
details: validateUser.errors.map(err => ({
campo: err.instancePath || err.params?.missingProperty,
mensaje: err.message,
})),
});
}
// req.body es válido, continuar con la lógica de negocio
const user = createUser(req.body);
res.status(201).json(user);
}
Salida cuando hay errores:
{
"error": "Datos inválidos",
"details": [
{ "campo": "/email", "mensaje": "must match format "email"" },
{ "campo": "age", "mensaje": "must be >= 18" }
]
}
Schemas reutilizables con $ref
Cuando tienes schemas que se repiten (por ejemplo, el objeto Address en múltiples endpoints), usa $ref para reutilizarlos:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"zip": { "type": "string", "pattern": "^\\d{5}$" }
},
"required": ["street", "city", "zip"]
}
},
"type": "object",
"properties": {
"name": { "type": "string" },
"billing_address": { "$ref": "#/$defs/Address" },
"shipping_address": { "$ref": "#/$defs/Address" }
}
}
En JavaScript con AJV:
// Schemas separados con referencias entre archivos
const addressSchema = {
$id: 'https://api.ejemplo.com/schemas/address',
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
zip: { type: 'string', pattern: '^\d{5}$' },
},
required: ['street', 'city', 'zip'],
};
const orderSchema = {
$id: 'https://api.ejemplo.com/schemas/order',
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
billing_address: { $ref: 'https://api.ejemplo.com/schemas/address' },
shipping_address: { $ref: 'https://api.ejemplo.com/schemas/address' },
},
};
ajv.addSchema(addressSchema);
const validateOrder = ajv.compile(orderSchema);
Schemas condicionales: if/then/else
A veces la validación de un campo depende del valor de otro. JSON Schema soporta lógica condicional:
{
"type": "object",
"properties": {
"payment_method": { "type": "string", "enum": ["card", "bank_transfer"] },
"card_number": { "type": "string" },
"iban": { "type": "string" }
},
"if": {
"properties": { "payment_method": { "const": "card" } }
},
"then": {
"required": ["card_number"]
},
"else": {
"required": ["iban"]
}
}
Si payment_method es "card", entonces card_number es obligatorio. Si no, iban es obligatorio.
Integración con TypeScript: inferencia automática de tipos
Con la librería json-schema-to-ts, puedes generar tipos TypeScript directamente desde tus schemas:
npm install json-schema-to-ts
import { FromSchema } from 'json-schema-to-ts';
const createUserSchema = {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string' },
age: { type: 'integer' },
role: { type: 'string', enum: ['admin', 'editor', 'viewer'] as const },
},
required: ['name', 'email'],
additionalProperties: false,
} as const; // 'as const' es necesario para la inferencia
type CreateUserRequest = FromSchema<typeof createUserSchema>;
// Resultado inferido:
// {
// name: string;
// email: string;
// age?: number;
// role?: 'admin' | 'editor' | 'viewer';
// }
Ahora tu schema es la fuente de verdad tanto para validación en runtime como para tipos TypeScript. Si cambias el schema, los tipos se actualizan automáticamente.
JSON Schema en Fastify: validación automática
Fastify (el framework Node.js más rápido) integra AJV de forma nativa. Defines el schema en la ruta y Fastify valida automáticamente:
import Fastify from 'fastify';
const app = Fastify();
const createUserSchema = {
body: {
type: 'object',
properties: {
name: { type: 'string', minLength: 2 },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
email: { type: 'string' },
},
},
},
};
app.post('/users', { schema: createUserSchema }, async (request, reply) => {
// request.body ya está validado y tipado
const user = await createUser(request.body);
reply.status(201).send(user);
});
Si la petición no cumple el schema, Fastify devuelve automáticamente un 400 Bad Request con detalles del error, sin que escribas ni una línea de código de validación.
JSON Schema como documentación: OpenAPI
OpenAPI (Swagger) usa JSON Schema para describir las APIs. Si ya tienes tus schemas, puedes generar documentación interactiva automáticamente:
# openapi.yml
openapi: 3.1.0
paths:
/users:
post:
summary: Crear usuario
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
CreateUserRequest:
type: object
properties:
name:
type: string
minLength: 2
email:
type: string
format: email
required: [name, email]
Con herramientas como Swagger UI o Redoc, este YAML genera automáticamente una interfaz interactiva donde puedes probar la API.
Estrategia de validación por capas
No pongas toda la validación en JSON Schema. Úsalo para lo que hace bien: estructura y tipos. Separa responsabilidades:
| Capa | Herramienta | Qué valida |
|---|---|---|
| Estructura | JSON Schema | Tipos, formatos, campos requeridos, rangos |
| Negocio | Tu código | "El email ya existe", "El usuario tiene permiso" |
| Base de datos | Constraints SQL | Unicidad, integridad referencial |
| Frontend | Mismos schemas | Validación en tiempo real antes de enviar |
async function createUserHandler(req, res) {
// Capa 1: JSON Schema (estructura)
const valid = validateUser(req.body);
if (!valid) return res.status(400).json({ errors: validateUser.errors });
// Capa 2: Lógica de negocio
const existingUser = await db.users.findByEmail(req.body.email);
if (existingUser) {
return res.status(409).json({ error: 'El email ya está registrado' });
}
// Capa 3: Base de datos (validará unicidad como constraint adicional)
const user = await db.users.create(req.body);
res.status(201).json(user);
}
Resumen
JSON Schema es una inversión que se amortiza rápido:
- Previene bugs de datos mal formateados antes de llegar a la base de datos
- Genera documentación automáticamente (OpenAPI/Swagger)
- Comparte validación entre frontend y backend usando el mismo schema
- Mejora mensajes de error: en lugar de "Internal Server Error", el cliente recibe exactamente qué campo está mal y por qué
- Sirve como contrato: el schema es un contrato explícito entre quien produce los datos y quien los consume
Empieza con un schema simple para tu endpoint más usado. Con AJV compilado, la validación es tan rápida que no notarás el overhead. La próxima vez que un "adulto" llegue donde esperabas un número, tu API lo rechazará elegantemente en el primer milisegundo.
Prueba a formatear y validar tus JSONs mientras diseñas schemas con nuestra herramienta Validador JSON.
関連記事
Cron Expressions: Entiende y Escribe Jobs Programados sin Errores
Aprende a leer y escribir cron expressions desde cero, evita los errores más comunes, y domina la sintaxis extendida de herramientas modernas como node-cron o GitHub Actions.
OAuth 2.0 vs JWT: Cuándo Usar Cada Uno (y Cuándo Usarlos Juntos)
Entiende la diferencia real entre OAuth 2.0 y JWT, por qué no son lo mismo, y cómo combinarlos correctamente en autenticación moderna.