JSON Toolkit

JSON Schema: de lo básico a producción

En resumen: Hay muchas palabras clave, pero solo una docena se usan a diario. Domina type/properties/required/$ref, gestiona bien los mensajes de error y cubrirás el ochenta por ciento de los casos reales.

Intermedio~14 min de lecturaPublicado el 2026-10-30Actualizado el 2026-10-30
Portada de JSON Schema: de lo básico a producción

En resumen: fija primero $schema a 2020-12 y luego extrae las estructuras compartidas con $defs + $ref; usa oneOf solo cuando deba coincidir exactamente una rama, y prefiere discriminadores o anyOf en todos los demás casos.

Un esquema no es un apéndice de la documentación; es un activo que se puede ejecutar, probar y generar.

1. Aclara primero la versión

Versión Valor de $schema Estado Notas
draft-04 http://json-schema.org/draft-04/schema# Obsoleta Proyectos heredados y parte de las herramientas de OpenAPI 2.0
draft-07 http://json-schema.org/draft-07/schema# Muy usada La base de facto de OpenAPI 3.0
2019-09 https://json-schema.org/draft/2019-09/schema Disponible Introdujo $defs y el vocabulario de anotación
2020-12 https://json-schema.org/draft/2020-12/schema Recomendada actualmente Introdujo prefixItems y cambió la semántica de items

Usa 2020-12 para proyectos nuevos. Ten en cuenta que en 2020-12 items solo restringe los "elementos restantes" — las declaraciones de estilo tupla se trasladaron a prefixItems. Esa es la trampa más habitual al migrar desde draft-07.

2. Un esquema mínimo usable

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/user.json",
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "string", "minLength": 1 },
    "name": { "type": "string", "minLength": 1, "maxLength": 64 },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
  },
  "additionalProperties": false
}

Este esquema cubre las cuatro preocupaciones más comunes: restricciones de tipo, campos obligatorios, límites de rango y restricciones de elementos de array.

3. Mapa de palabras clave

3.1 Generales

Palabra clave Propósito Ojo con
type El tipo; puede ser un array "type": ["string", "null"] significa anulable
enum Valores enumerados Los valores deben ser únicos
const Una restricción de valor único Útil para valores fijos como un número de versión
$ref Referencia a otro esquema Referencia el esquema completo y no se fusiona con las palabras clave hermanas del objeto que referencia

3.2 Números

multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum. Fíjate en que en draft-04 exclusiveMaximum era un booleano (usado junto con maximum); solo a partir de draft-07 se convirtió en una palabra clave numérica independiente — lo que hace que muchos "esquemas copiados de documentación antigua" fallen en modo estricto.

3.3 Cadenas

minLength, maxLength, pattern (una expresión regular ECMA-262) y format (una anotación).

minLength cuenta puntos de código Unicode, no unidades UTF-16 ni bytes. Validar con string.length de JavaScript dará un resultado distinto.

3.4 Arrays

Palabra clave Propósito
prefixItems La restricción de estilo tupla de 2020-12, que hace coincidir los elementos uno a uno por índice
items Una restricción uniforme sobre los elementos restantes (2020-12)
contains + minContains/maxContains Cuántos elementos coincidentes debe haber como mínimo/como máximo
uniqueItems Los elementos deben ser todos distintos (comparación profunda; evitar en arrays grandes)
minItems / maxItems Restricciones de longitud

3.5 Objetos

properties, required, patternProperties, additionalProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas.

Tres ideas equivocadas de alta frecuencia:

  1. required solo exige que la clave exista: {"name": ""} es válido. Para rechazar una cadena vacía debes añadir minLength: 1;
  2. additionalProperties: false amplía los problemas de compatibilidad: un campo extra de un cliente hace fallar todo el payload, así que evolucionar la API obliga a todos los consumidores a actualizarse al mismo tiempo;
  3. properties no restringe las claves no declaradas — por defecto se permiten.

3.6 Composición lógica

{
  "oneOf": [
    { "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
    { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
  ]
}
Palabra clave Semántica Fallo típico
allOf Deben cumplirse todas Varias ramas imponen restricciones contradictorias sobre el mismo campo
anyOf Debe cumplirse al menos una El mensaje de error es difícil de rastrear hasta una rama concreta
oneOf Debe cumplirse exactamente una Los datos coinciden con dos ramas y falla, aunque "parezca obviamente correcto"
not No debe cumplirse Se usa poco, y tiene los mensajes de error menos amigables

oneOf es la palabra clave con más probabilidad de producir "datos válidos que fallan la validación". Si las restricciones de las ramas se solapan (por ejemplo, ambas permiten campos extra), un mismo registro coincide con varias ramas y oneOf falla sin más. En ese caso, cambia a anyOf, o usa un campo discriminador con const dentro de las ramas.

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": { "properties": { "kind": { "const": "image" } } },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

El estilo con discriminador es más legible que oneOf y produce mejores errores.

4. División y reutilización

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "address": {
      "type": "object",
      "required": ["city"],
      "properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
    }
  },
  "type": "object",
  "required": ["total", "shipTo"],
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "shipTo": { "$ref": "#/$defs/address" }
  }
}
  • $defs es la forma introducida en 2019-09; draft-07 usa definitions;
  • Las referencias entre archivos usan $id relativos, pero no dependas de obtener esquemas remotos en tiempo de ejecución en producción — empaquétalos como archivos estáticos;
  • No puedes añadir restricciones junto a una referencia (el comportamiento de $ref con palabras clave hermanas varía según la versión). Cuando necesites composición, usa allOf.

5. Validación en tiempo de ejecución con Ajv

import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };

const ajv = new Ajv2020({
  allErrors: true,
  strict: true,
  allowUnionTypes: true,
});
addFormats(ajv);

const validateOrder = ajv.compile(orderSchema);

export function assertOrder(input: unknown): void {
  if (validateOrder(input)) {
    return;
  }
  const detail = (validateOrder.errors ?? []).map((errorItem) => ({
    path: errorItem.instancePath || '/',
    keyword: errorItem.keyword,
    message: errorItem.message,
    params: errorItem.params,
  }));
  throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}

Puntos clave:

  • Llama a compile una sola vez, en el nivel superior del módulo; recompilar en cada petición convierte una validación de 10 ms en cientos de milisegundos;
  • strict: true saca a la luz pronto las palabras clave mal escritas (por ejemplo minlength en lugar de minLength), así que merece la pena activarlo;
  • Solo ajv-formats hace que format: "email" y format: "date-time" surtan efecto de verdad;
  • 2020-12 requiere el punto de entrada ajv/dist/2020 — no uses la exportación por defecto.

6. Cómo diseñar los mensajes de error

Lo que el validador te da es "qué palabra clave ha fallado"; lo que el usuario quiere es "qué casilla está mal". Merece la pena añadir una capa de mapeo:

const FIELD_LABEL: Record<string, string> = {
  '/name': 'Name',
  '/age': 'Age',
  '/tags': 'Tags',
};

/**
 * Convert Ajv errors into display-ready messages.
 * @param errors The list of Ajv errors
 * @returns An array of display messages
 */
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
  return errors.map((errorItem) => {
    const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
    if (errorItem.keyword === 'required') {
      const missing = (errorItem.params as { missingProperty: string }).missingProperty;
      return `${label} is missing the required field ${missing}`;
    }
    if (errorItem.keyword === 'minLength') {
      return `${label} is too short`;
    }
    return `${label} failed validation (${errorItem.keyword})`;
  });
}

En el lado de la API, mantén una estructura legible por máquina para que el frontend pueda decidir cómo presentarla:

{
  "type": "https://bbdrug.com/problems/validation",
  "title": "Request data validation failed",
  "status": 422,
  "errors": [
    { "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
  ]
}

7. Rendimiento y trampas

  • El coste de validación crece con la complejidad del esquema y el tamaño de los datos; varias ramas oneOf provocan una explosión combinatoria, que se nota mucho con miles de registros;
  • uniqueItems realiza una comparación profunda O(n²) sobre el array; para arrays grandes, deduplica en la capa de negocio;
  • Evita patrones de retroceso catastrófico en pattern (como (a+)+$); este es el punto de entrada clásico para ReDoS;
  • Cuando additionalProperties: false se combina con allOf, los campos declarados en un subesquema se consideran "adicionales" por el padre, así que necesitas fusionarlos en un único esquema o usar unevaluatedProperties.

8. Lista de comprobación

  • [ ] Fija 2020-12 y declara $schema en la parte superior del archivo;
  • [ ] Extrae las estructuras compartidas con $defs + $ref en lugar de copiar y pegar;
  • [ ] Prefiere discriminadores a oneOf;
  • [ ] Reutiliza el resultado de compile del validador y activa el modo estricto;
  • [ ] Documenta con claridad si format se aplica;
  • [ ] Las respuestas de error proporcionan la ruta, la palabra clave y un mensaje para el usuario;
  • [ ] Pon el esquema en CI: ejecuta los casos de JSON Schema Test Suite como pruebas de regresión.

Preguntas frecuentes

¿Cómo debería elegir un draft de JSON Schema?

Usa 2020-12 para proyectos nuevos y declara $schema en la parte superior del archivo. Si el esquema va a OpenAPI 3.1, también debe ser 2020-12, porque 3.1 incorpora directamente esa versión; mantén draft-07 solo para proyectos todavía atados a cadenas de herramientas antiguas. Ambos se comportan de forma distinta con items y con las palabras clave hermanas de $ref, así que no los mezcles en un mismo conjunto de esquemas.

¿Por qué additionalProperties: false amplía los problemas de compatibilidad?

Trata cada clave no declarada como un error, así que un campo extra de un cliente hace fallar todo el payload. Entonces todos los consumidores deben actualizarse al mismo tiempo cada vez que la API evoluciona, o los clientes antiguos serán rechazados. Los esquemas de configuración deberían cerrar los campos extra explícitamente, mientras que las API públicas necesitan sopesar el coste de compatibilidad.

¿Debería usar oneOf o anyOf?

Usa oneOf solo cuando los datos deban coincidir exactamente con una rama. Si las restricciones de las ramas pueden cumplirse a la vez, un mismo registro coincide con varias ramas y oneOf falla sin más; cambia a anyOf, o añade un discriminador const dentro de cada rama.

Referencias

Artículos relacionados

Sincronización bidireccional entre esquema y tipos de TypeScript

Los tipos y los esquemas cubren cada uno un segmento: uno en tiempo de compilación, otro en tiempo de ejecución. La pregunta no es cuál elegir, sino cómo mantener ambos sincronizados de forma automática.

Intermedio~11 min de lecturaActualizado el 2026-10-30

Puntos clave para diseñar contratos de datos de API JSON

Seis cosas son muy difíciles de cambiar una vez publicadas: los envoltorios de respuesta, los formatos de error, la forma de la paginación, la representación de IDs y tiempo, la semántica de null frente a ausente, y la evolución de versiones.

Intermedio~13 min de lecturaActualizado el 2026-10-30

Sin additionalProperties, la validación de esquema no valida nada

Una validación exitosa no significa datos limpios. Pydantic ignora los campos desconocidos por defecto, Zod los elimina y Ajv los deja pasar — los parámetros alucinados y las claves mal escritas atraviesan en silencio. Este artículo muestra las tres formas de aceptación silenciosa y da recetas completas: cerrar objetos, controlar la coerción y apretar cada nivel de anidación.

Intermediate~8 min de lecturaActualizado el 2026-11-02