Introducción: De la Teoría a la Práctica con JSON Schema
JSON Schema es una herramienta poderosa para validar la estructura de datos JSON, pero su verdadero potencial se revela cuando se aplica a escenarios del mundo real. Muchos desarrolladores comprenden las palabras clave básicas como type, required y properties, pero se encuentran con dificultades al enfrentarse a requisitos complejos de producción: campos que pueden ser nulos, validaciones que dependen del valor de otro campo, esquemas reutilizables mediante referencias, o patrones de texto que deben cumplir formatos específicos.
En este artículo presentamos cuatro escenarios completos y realistas donde JSON Schema resuelve problemas concretos de validación de datos. Cada ejemplo incluye el esquema completo, explicaciones detalladas de cada decisión de diseño y documentos JSON válidos e inválidos para ilustrar el comportamiento esperado. Puedes probar todos los ejemplos directamente en nuestro validador JSON Schema gratuito mientras sigues la lectura.
Ejemplo 1: API de Registro de Usuarios
El registro de usuarios es uno de los puntos de entrada más críticos de cualquier aplicación. Los datos que llegan desde formularios web o aplicaciones móviles deben validarse rigurosamente antes de almacenarse en la base de datos. Un esquema JSON Schema bien diseñado actúa como primera línea de defensa contra datos malformados, inyecciones y errores de tipado.
Esquema Completo de Registro
A continuación se muestra un esquema que cubre los campos típicos de un formulario de registro con validaciones robustas:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "RegistroUsuario",
"description": "Esquema para el endpoint POST /api/v1/users/register",
"type": "object",
"required": ["username", "email", "password", "acceptTerms"],
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 30,
"pattern": "^[a-zA-Z0-9_]+$",
"description": "Solo letras, números y guiones bajos"
},
"email": {
"type": "string",
"format": "email",
"maxLength": 254
},
"password": {
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$"
},
"displayName": {
"type": ["string", "null"],
"minLength": 1,
"maxLength": 60
},
"phoneNumber": {
"type": "string",
"pattern": "^\\+[1-9]\\d{6,14}$"
},
"birthDate": {
"type": "string",
"format": "date"
},
"acceptTerms": {
"type": "boolean",
"const": true
},
"referralCode": {
"type": "string",
"pattern": "^REF-[A-Z0-9]{8}$"
}
},
"additionalProperties": false
}
Análisis del Esquema de Registro
Este esquema ilustra varias técnicas fundamentales que aparecen constantemente en APIs de producción:
- Patrón regex para username: La expresión
^[a-zA-Z0-9_]+$garantiza que el nombre de usuario contenga únicamente caracteres alfanuméricos y guiones bajos. Esto previene inyecciones y simplifica el uso del campo como parte de URLs de perfil. - Validación de contraseña con lookahead: El patrón
^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$exige al menos una minúscula, una mayúscula y un dígito. Los lookaheads positivos verifican cada requisito sin consumir caracteres, permitiendo que se combinen múltiples reglas en un solo patrón. - Campo nulable con
["string", "null"]: El campodisplayNamees opcional y puede enviarse explícitamente comonull. Esto es diferente de omitir el campo: permite que el frontend indique intencionalmente que el usuario no desea un nombre visible. - Valor constante con
const: El campoacceptTermsdebe ser exactamentetrue. Usarconsten lugar deenum: [true]es más semántico y expresa claramente que solo se acepta un valor específico. - Formato de teléfono internacional: El patrón
^\\+[1-9]\\d{6,14}$sigue el formato E.164 para números de teléfono internacionales, comenzando con el signo más y el código de país. - additionalProperties: false: Rechaza cualquier campo no definido en el esquema. Esto es crucial en APIs públicas para evitar que clientes envíen datos inesperados que podrían procesarse incorrectamente.
Ejemplos de Documentos Válidos e Inválidos
Un documento válido para este esquema sería:
{
"username": "maria_garcia",
"email": "[email protected]",
"password": "Segura2026!",
"displayName": "María García",
"acceptTerms": true
}
En cambio, el siguiente documento falla la validación porque la contraseña no contiene mayúsculas, el campo acceptTerms es false y el nombre de usuario contiene espacios:
{
"username": "maria garcia",
"email": "[email protected]",
"password": "solopalabras",
"acceptTerms": false
}
Ejemplo 2: Catálogo de Productos de E-Commerce
Los catálogos de productos en plataformas de comercio electrónico manejan una gran diversidad de tipos de artículos, cada uno con atributos específicos. Un libro tiene ISBN y autor, mientras que una prenda de vestir tiene talla y color. JSON Schema permite modelar esta variabilidad de forma elegante usando validación condicional y referencias reutilizables.
Esquema con Validación Condicional y $ref
El siguiente esquema utiliza definitions (o $defs) para definir subesquemas reutilizables y if/then para aplicar validaciones específicas según la categoría del producto:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ProductoCatalogo",
"type": "object",
"required": ["sku", "name", "price", "category", "stock"],
"definitions": {
"monetaryAmount": {
"type": "number",
"minimum": 0,
"multipleOf": 0.01
},
"dimensions": {
"type": "object",
"required": ["width", "height", "unit"],
"properties": {
"width": { "type": "number", "exclusiveMinimum": 0 },
"height": { "type": "number", "exclusiveMinimum": 0 },
"depth": { "type": "number", "exclusiveMinimum": 0 },
"unit": { "type": "string", "enum": ["cm", "in", "mm"] }
}
}
},
"properties": {
"sku": {
"type": "string",
"pattern": "^[A-Z]{3}-\\d{6}$"
},
"name": {
"type": "string",
"minLength": 3,
"maxLength": 200
},
"description": {
"type": ["string", "null"],
"maxLength": 5000
},
"price": { "$ref": "#/definitions/monetaryAmount" },
"compareAtPrice": {
"oneOf": [
{ "$ref": "#/definitions/monetaryAmount" },
{ "type": "null" }
]
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "books", "home", "sports"]
},
"stock": {
"type": "integer",
"minimum": 0
},
"tags": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true,
"maxItems": 20
},
"dimensions": { "$ref": "#/definitions/dimensions" },
"isbn": { "type": "string", "pattern": "^(978|979)\\d{10}$" },
"author": { "type": "string" },
"size": { "type": "string", "enum": ["XS", "S", "M", "L", "XL", "XXL"] },
"color": { "type": "string" }
},
"allOf": [
{
"if": {
"properties": { "category": { "const": "books" } },
"required": ["category"]
},
"then": {
"required": ["isbn", "author"]
}
},
{
"if": {
"properties": { "category": { "const": "clothing" } },
"required": ["category"]
},
"then": {
"required": ["size", "color"]
}
}
]
}
Desglose de Técnicas Utilizadas
Este esquema demuestra técnicas avanzadas esenciales para sistemas de e-commerce:
- Referencias con $ref: La definición
monetaryAmountse reutiliza tanto enpricecomo encompareAtPrice. Esto evita duplicar la lógica de validación de importes monetarios. Si los requisitos cambian (por ejemplo, aceptar hasta tres decimales), solo se modifica la definición una vez. - multipleOf para precisión monetaria:
"multipleOf": 0.01asegura que los precios tengan como máximo dos decimales. Esto previene valores como19.999que causarían problemas en la facturación. - enum para valores controlados: Las categorías, tallas y unidades de medida usan
enumpara restringir los valores a un conjunto predefinido. Esto garantiza consistencia en la base de datos y facilita las búsquedas con filtros. - Validación condicional con if/then: Cuando la categoría es
"books", los camposisbnyauthorse vuelven obligatorios. Cuando es"clothing", se requierensizeycolor. Esta técnica del Draft 7 reemplaza patrones más complejos cononeOfque eran necesarios en versiones anteriores. - Campo nulable con oneOf: El campo
compareAtPriceusaoneOfpara aceptar un importe monetario onull. Esto permite que un producto tenga o no un precio de comparación (precio tachado) sin usar el tipo múltiple directamente en la referencia. - uniqueItems en arrays: Los tags del producto deben ser únicos, evitando duplicados que ensuciarían las búsquedas y filtros.
Producto Válido: Ejemplo de Libro
{
"sku": "BOK-000142",
"name": "Patrones de Diseño en JavaScript Moderno",
"description": "Guía completa de patrones de diseño aplicados a ES2024+",
"price": 34.99,
"compareAtPrice": 44.99,
"category": "books",
"stock": 230,
"tags": ["javascript", "patrones", "programación"],
"isbn": "9781234567890",
"author": "Ana López"
}
Ejemplo 3: Archivos de Configuración de Aplicaciones
Los archivos de configuración son el punto donde más frecuentemente ocurren errores en despliegues: un puerto con formato incorrecto, una URL de base de datos mal escrita o un nivel de log inexistente pueden provocar que una aplicación falle en el arranque sin mensajes claros. Validar la configuración con JSON Schema antes de iniciar la aplicación detecta estos problemas de forma inmediata.
Esquema de Configuración de Aplicación
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AppConfig",
"description": "Configuración de aplicación Node.js con base de datos y caché",
"type": "object",
"required": ["server", "database", "logging"],
"properties": {
"server": {
"type": "object",
"required": ["port", "host"],
"properties": {
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"host": {
"type": "string",
"minLength": 1
},
"cors": {
"type": "object",
"properties": {
"origins": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"minItems": 1
},
"methods": {
"type": "array",
"items": {
"type": "string",
"enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
}
}
}
},
"rateLimit": {
"type": "object",
"properties": {
"windowMs": { "type": "integer", "minimum": 1000 },
"maxRequests": { "type": "integer", "minimum": 1 }
}
}
}
},
"database": {
"type": "object",
"required": ["host", "port", "name"],
"properties": {
"host": { "type": "string", "minLength": 1 },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
"username": { "type": "string" },
"password": { "type": "string" },
"ssl": { "type": "boolean", "default": false },
"pool": {
"type": "object",
"properties": {
"min": { "type": "integer", "minimum": 0, "default": 2 },
"max": { "type": "integer", "minimum": 1, "default": 10 },
"idleTimeoutMs": { "type": "integer", "minimum": 0 }
}
}
}
},
"cache": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"provider": { "type": "string", "enum": ["redis", "memcached", "memory"] },
"ttlSeconds": { "type": "integer", "minimum": 0 },
"connectionString": { "type": ["string", "null"] }
},
"if": {
"properties": { "enabled": { "const": true } },
"required": ["enabled"]
},
"then": {
"required": ["provider"]
}
},
"logging": {
"type": "object",
"required": ["level"],
"properties": {
"level": {
"type": "string",
"enum": ["debug", "info", "warn", "error", "fatal"]
},
"format": {
"type": "string",
"enum": ["json", "text", "pretty"],
"default": "json"
},
"filePath": {
"type": ["string", "null"]
}
}
},
"features": {
"type": "object",
"patternProperties": {
"^[a-zA-Z][a-zA-Z0-9_]*$": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
Análisis de las Validaciones de Configuración
Este esquema incorpora patrones frecuentes en configuraciones de producción:
- Rangos para puertos de red: Los puertos del servidor y la base de datos se validan con
minimum: 1ymaximum: 65535, el rango válido para puertos TCP/UDP. Esto detecta errores como usar el puerto0o valores que exceden el límite. - Validación condicional en caché: Cuando
cache.enabledestrue, el campoproviderse vuelve obligatorio. Sin embargo, cuando la caché está deshabilitada, no es necesario especificar un proveedor. Esta lógica condicional evita requerir configuración innecesaria. - patternProperties para feature flags: El objeto
featurespermite cualquier nombre de propiedad que cumpla el patrón de identificador válido, pero el valor debe ser siempre un booleano. Combinado conadditionalProperties: false, esto impide nombres de feature flags con caracteres especiales. - Valores por defecto con default: Los campos como
pool.min,pool.maxylogging.formatincluyen valores por defecto documentados. Aunque JSON Schema no aplica los valores por defecto durante la validación (es tarea de la aplicación), los documenta como parte del contrato. - Nombres de base de datos con patrón: El campo
database.nameusa el patrón^[a-zA-Z_][a-zA-Z0-9_]*$que corresponde a los identificadores válidos en la mayoría de motores SQL, previniendo nombres que causarían errores de sintaxis.
Configuración Válida de Ejemplo
{
"server": {
"port": 3000,
"host": "0.0.0.0",
"cors": {
"origins": ["https://miapp.com", "https://admin.miapp.com"],
"methods": ["GET", "POST", "PUT", "DELETE"]
}
},
"database": {
"host": "db.interna.local",
"port": 5432,
"name": "app_produccion",
"username": "app_user",
"ssl": true,
"pool": { "min": 5, "max": 20, "idleTimeoutMs": 30000 }
},
"cache": {
"enabled": true,
"provider": "redis",
"ttlSeconds": 3600,
"connectionString": "redis://cache.interna.local:6379"
},
"logging": {
"level": "info",
"format": "json",
"filePath": "/var/log/app/output.log"
},
"features": {
"nuevoCheckout": true,
"betaDashboard": false
}
}
Ejemplo 4: Configuración de Pipeline CI/CD
Los pipelines de integración continua y despliegue continuo son el corazón de la entrega moderna de software. Un error en la configuración del pipeline puede provocar despliegues fallidos, pruebas que no se ejecutan o artefactos que se publican en el entorno equivocado. Validar la configuración del pipeline con JSON Schema proporciona retroalimentación inmediata antes de que el pipeline se ejecute.
Esquema de Pipeline CI/CD
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PipelineConfig",
"description": "Configuración de pipeline CI/CD",
"type": "object",
"required": ["version", "stages", "jobs"],
"definitions": {
"envVar": {
"type": "object",
"required": ["name", "value"],
"properties": {
"name": {
"type": "string",
"pattern": "^[A-Z_][A-Z0-9_]*$"
},
"value": {
"type": ["string", "number", "boolean"]
},
"secret": {
"type": "boolean",
"default": false
}
}
},
"artifact": {
"type": "object",
"required": ["path"],
"properties": {
"path": { "type": "string", "minLength": 1 },
"retentionDays": {
"type": "integer",
"minimum": 1,
"maximum": 90
},
"compressFormat": {
"type": "string",
"enum": ["gzip", "zip", "none"]
}
}
}
},
"properties": {
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+$"
},
"stages": {
"type": "array",
"items": {
"type": "string",
"enum": ["build", "test", "lint", "security", "deploy-staging", "deploy-production"]
},
"minItems": 1,
"uniqueItems": true
},
"globalEnv": {
"type": "array",
"items": { "$ref": "#/definitions/envVar" }
},
"jobs": {
"type": "object",
"patternProperties": {
"^[a-z][a-z0-9-]*$": {
"type": "object",
"required": ["stage", "image", "commands"],
"properties": {
"stage": {
"type": "string"
},
"image": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._/-]*(:[a-zA-Z0-9._-]+)?$"
},
"commands": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"minItems": 1
},
"env": {
"type": "array",
"items": { "$ref": "#/definitions/envVar" }
},
"artifacts": {
"type": "array",
"items": { "$ref": "#/definitions/artifact" }
},
"dependsOn": {
"type": "array",
"items": { "type": "string" }
},
"timeout": {
"type": "integer",
"minimum": 60,
"maximum": 7200,
"default": 600
},
"retryCount": {
"type": "integer",
"minimum": 0,
"maximum": 3
},
"allowFailure": {
"type": "boolean",
"default": false
},
"onlyWhen": {
"type": "object",
"properties": {
"branches": {
"type": "array",
"items": { "type": "string" }
},
"changes": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}
},
"additionalProperties": false,
"minProperties": 1
}
},
"additionalProperties": false
}
Análisis del Esquema de Pipeline
Este esquema aborda los desafíos específicos de la configuración de pipelines CI/CD:
- Definiciones reutilizables con $ref: Las variables de entorno (
envVar) y los artefactos (artifact) se definen una vez y se referencian tanto a nivel global como dentro de cada job. Esto garantiza que las mismas reglas de validación se apliquen consistentemente en toda la configuración. - Nombres de variables con convención SCREAMING_SNAKE_CASE: El patrón
^[A-Z_][A-Z0-9_]*$para nombres de variables de entorno impone la convención estándar de sistemas Unix, previniendo nombres comomi-variableque causarían problemas en shell scripts. - Validación de imágenes Docker: El campo
imagevalida el formato de referencias a imágenes de contenedores, aceptando formatos comonode:18-alpine,registry.ejemplo.com/mi-imagen:latestopython:3.11. - Tipos múltiples para valores de entorno: El campo
valuede las variables aceptastring,numberoboolean, reflejando la realidad de que los valores de configuración pueden ser de diversos tipos antes de convertirse a cadenas en el entorno de ejecución. - patternProperties para jobs dinámicos: Los nombres de jobs deben comenzar con una letra minúscula y contener solo letras minúsculas, números y guiones. Esto proporciona flexibilidad para nombrar los jobs a la vez que se imponen convenciones de nomenclatura.
- Límites de tiempo razonables: El
timeouttiene un mínimo de 60 segundos y un máximo de 7200 (dos horas), evitando tanto timeouts demasiado cortos que causen falsos errores como timeouts excesivos que desperdicien recursos. - Control de reintentos y fallos: Los campos
retryCountyallowFailurepermiten configurar la resiliencia de cada job individualmente, una necesidad común cuando ciertos pasos como las pruebas de integración pueden ser inestables.
Pipeline Completo de Ejemplo
{
"version": "2.0",
"stages": ["build", "test", "lint", "deploy-staging"],
"globalEnv": [
{ "name": "NODE_ENV", "value": "test" },
{ "name": "CI", "value": true }
],
"jobs": {
"build-app": {
"stage": "build",
"image": "node:20-alpine",
"commands": [
"npm ci",
"npm run build"
],
"artifacts": [
{ "path": "dist/", "retentionDays": 7, "compressFormat": "gzip" }
],
"timeout": 300
},
"run-tests": {
"stage": "test",
"image": "node:20-alpine",
"commands": [
"npm ci",
"npm test -- --coverage"
],
"dependsOn": ["build-app"],
"retryCount": 2,
"env": [
{ "name": "DATABASE_URL", "value": "postgres://test:test@db:5432/test", "secret": true }
]
},
"lint-check": {
"stage": "lint",
"image": "node:20-alpine",
"commands": [
"npm ci",
"npm run lint"
],
"allowFailure": true,
"timeout": 120
},
"deploy-staging": {
"stage": "deploy-staging",
"image": "registry.ejemplo.com/deploy-tools:1.5",
"commands": [
"deploy --env staging --version $CI_COMMIT_SHA"
],
"dependsOn": ["run-tests", "lint-check"],
"onlyWhen": {
"branches": ["main", "develop"]
}
}
}
}
Patrones Transversales y Buenas Prácticas
A lo largo de los cuatro ejemplos hemos aplicado patrones que son universales en el diseño de esquemas JSON Schema para producción. A continuación los resumimos como referencia rápida.
Campos Nulables: Cuándo y Cómo Usarlos
JSON Schema ofrece dos formas de representar campos nulables. La primera es usando un arreglo de tipos: "type": ["string", "null"]. Esta es la forma más directa y funciona en todos los drafts. La segunda es mediante oneOf combinado con $ref, como vimos en el campo compareAtPrice del catálogo. Esta forma es preferible cuando el tipo no nulo es una referencia reutilizable.
Es importante distinguir entre un campo nulable y un campo opcional. Un campo opcional simplemente no aparece en el objeto. Un campo nulable puede estar presente con valor null. Ambos conceptos pueden combinarse: un campo puede ser opcional (no está en required) y, cuando está presente, aceptar null como valor válido.
Validación Condicional: if/then/else
La validación condicional del Draft 7 permite crear esquemas que se adaptan según los valores de los datos. El patrón básico es: if define una condición, then aplica restricciones adicionales cuando la condición se cumple, y else (opcional) aplica restricciones cuando no se cumple. Para múltiples condiciones independientes, se envuelven en allOf como mostramos en el esquema de productos.
Un error frecuente es olvidar el required dentro del if. Sin él, un objeto que no tenga la propiedad evaluada no cumplirá la condición y se aplicará la rama else (o ninguna si no hay else). Esto puede causar que la validación condicional nunca se active.
Reutilización con $ref y definitions
La referencia $ref es el mecanismo principal de reutilización en JSON Schema. Permite apuntar a cualquier parte del esquema usando un puntero JSON. La convención es colocar los subesquemas reutilizables en definitions (Draft 7) o $defs (Draft 2019-09+). Las referencias pueden ser locales (#/definitions/nombre) o remotas (https://ejemplo.com/schemas/base.json), aunque las remotas requieren que el validador soporte resolución de URIs.
Una buena práctica es extraer a definitions cualquier subesquema que se use dos o más veces, o que represente un concepto de dominio claro (como un importe monetario o unas dimensiones físicas), incluso si solo se usa una vez, para mejorar la legibilidad del esquema.
Patrones Regex: Validar sin Complicar
Los patrones regex en JSON Schema siguen la sintaxis ECMA-262 (la misma que JavaScript). Algunos consejos prácticos para su uso efectivo:
- Siempre anclar los patrones con
^y$para validar la cadena completa, no solo una subcadena. - Usar clases de caracteres simples en lugar de lookaheads complejos cuando sea posible, ya que no todos los validadores soportan todas las características de regex.
- Documentar los patrones con el campo
descriptionotitlepara que otros desarrolladores entiendan el formato esperado sin descifrar la expresión regular. - Probar los patrones con datos límite: cadenas vacías, caracteres Unicode, entradas extremadamente largas.
Enum: Control Total sobre Valores Permitidos
La palabra clave enum restringe un valor a un conjunto finito de opciones. Es ideal para campos como estados, categorías, roles y niveles. A diferencia de pattern, que valida la forma de una cadena, enum valida el valor exacto y funciona con cualquier tipo, no solo cadenas. Se puede combinar enum con type para mayor claridad, aunque enum por sí solo ya implica los tipos de los valores listados.
Conclusión: Esquemas como Documentación Viva
Los cuatro ejemplos que hemos explorado demuestran que JSON Schema va mucho más allá de validar tipos básicos. En escenarios de producción, un esquema bien diseñado actúa como documentación viva de los contratos de datos, herramienta de prevención de errores y guía para desarrolladores que necesitan entender la estructura esperada de los datos.
Las técnicas clave que hemos cubierto, incluyendo campos nulables, validación condicional con if/then, referencias con $ref, enumeraciones con enum y patrones regex, cubren la gran mayoría de necesidades de validación en aplicaciones reales. La inversión inicial en diseñar esquemas completos se recupera rápidamente en forma de menos bugs en producción, onboarding más rápido y APIs más robustas.
Te invitamos a tomar estos esquemas como punto de partida, adaptarlos a tus propios proyectos y probarlos en nuestro validador JSON Schema en línea. Recuerda que un buen esquema no solo valida datos: comunica intenciones, documenta restricciones y protege la integridad de tu sistema.