tutoriales·9 分で読める

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.

DevToolsHub Team
·
JSON Schema: Valida Tus APIs Antes de Desplegarlas

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.

#json-schema#api#validación#javascript#typescript

関連記事