Saltar a contenido

5.1 — JSON Schema y herramientas de validación

El problema de confiar ciegamente en los datos

Piensa en esto: llevas horas construyendo una funcionalidad que muestra el precio de un producto. Todo funciona perfectamente en tus pruebas. Lo subes a producción y a los cinco minutos la app peta. ¿Por qué? Porque el precio llegó como "19.99" en lugar de 19.99. Una comilla de diferencia. Un string donde debería haber un número.

Bienvenido al mundo real de las APIs y el intercambio de datos.

No puedes controlar lo que te mandan sistemas externos. Pero sí puedes decidir qué aceptas y qué rechazas antes de que esos datos toquen tu código. Para eso existe JSON Schema: un estándar que te permite describir exactamente cómo debe ser un documento JSON y validar automáticamente que los datos cumplen esas reglas.

En pocas palabras: le pones un portero a tus datos.

Cómo se construye un JSON Schema

Un JSON Schema es él mismo un documento JSON. Lo que hace es describir las reglas que debe cumplir otro documento JSON. Vamos por partes.

El esqueleto básico

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": { },
  "required": [ ]
}

$schema le dice al validador qué versión del estándar estás usando. Hay varias versiones (Draft 7, Draft 2019-09...), pero Draft 7 es la más compatible con las herramientas que vas a usar, así que quédate con esa por ahora.

type define qué tipo de dato es el documento raíz. En la mayoría de casos será "object", pero también puede ser un array, un string, o cualquier otro tipo.

properties es donde defines cada campo del objeto: su nombre y las reglas que debe cumplir.

required es la lista negra de campos que no pueden faltar. Si llega un documento sin alguno de ellos, el validador lo rechaza sin contemplaciones.

Los tipos de dato disponibles

Tipo Qué acepta
string Texto
number Números con o sin decimales
integer Solo enteros, nada de decimales
boolean true o false
array Una lista de elementos
object Un conjunto de pares clave-valor
null Ausencia de valor

La diferencia entre number e integer parece obvia pero se olvida fácil: si un campo debería ser siempre un número entero (una edad, una cantidad de stock, un identificador), usa integer. Evitarás que alguien meta 18.7 como edad.

Añadiendo restricciones

Saber que un campo es un string a veces no es suficiente. Quizás ese string tiene que ser un email. O tiene que tener al menos tres caracteres. O tiene que seguir un formato concreto. Para todo eso tienes las restricciones.

Para textos:

minLength y maxLength controlan la longitud. Un campo nombre con "minLength": 1 evita que lleguen nombres vacíos. Parece una tontería hasta que ves que alguien manda "" y tu interfaz muestra un saludo al vacío.

format valida formatos especiales conocidos: "email", "date-time", "uri"... El validador sabe cómo debe ser cada uno y te ahorra escribir la lógica tú mismo.

Para números:

minimum y maximum definen el rango permitido. Un precio con "minimum": 0 evita que nadie meta un precio negativo. Un campo pegi con "minimum": 3, "maximum": 18 asegura que el valor está dentro del rango oficial.

Para formatos más específicos: pattern

Cuando ningún format estándar cubre lo que necesitas, puedes usar pattern con una expresión regular. Si no has trabajado con expresiones regulares antes, al principio parecen jeroglíficos, pero tienen una lógica muy clara una vez que pillas los símbolos básicos:

  • ^ marca el inicio del texto, $ marca el final. Si no los pones, el patrón puede coincidir con una parte del texto y no detectar errores.
  • [A-Z] es cualquier letra mayúscula. [0-9] es cualquier dígito.
  • {n} significa "exactamente n veces".

Con eso ya puedes construir patrones útiles. Por ejemplo, para validar una matrícula española antigua estilo "ABC1234":

{
  "type": "string",
  "pattern": "^[A-Z]{3}[0-9]{4}$"
}

Léelo en voz alta: "empieza con tres letras mayúsculas, seguidas de cuatro dígitos, y termina ahí". Si le mandas "AB1234" o "ABCD1234", falla.

Estructuras complejas

El mundo real no es plano. Un pedido tiene productos, cada producto tiene un precio y un nombre, y el pedido tiene una dirección de entrega que tiene calle, número y ciudad. JSON Schema maneja todo eso.

Objetos dentro de objetos

Una propiedad puede ser ella misma un objeto con sus propias reglas:

{
  "type": "object",
  "properties": {
    "direccion": {
      "type": "object",
      "properties": {
        "calle":  { "type": "string" },
        "numero": { "type": "integer" }
      },
      "required": ["calle", "numero"]
    }
  }
}

Puedes anidar todo lo que necesites. Cada nivel tiene sus propias reglas independientes.

Arrays de objetos

Si tienes una lista de elementos —los productos de un pedido, los comentarios de un post— usas items para describir cómo debe ser cada elemento:

{
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "nombre":   { "type": "string" },
      "cantidad": { "type": "integer", "minimum": 1 }
    },
    "required": ["nombre", "cantidad"]
  }
}

Todos los objetos del array se validarán contra ese mismo esquema. Si uno falla, el array entero falla.

Reutilizando definiciones con $ref

Cuando el mismo subesquema aparece en varios sitios (por ejemplo, la estructura de una dirección se usa tanto en el usuario como en el pedido), puedes definirla una vez en $defs y referenciarla con $ref. Evita duplicar código y hace el esquema mucho más mantenible. Esto lo veremos con más detalle más adelante.

Ejemplo completo: esquema de un producto

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Producto",
  "type": "object",
  "properties": {
    "nombre":  { "type": "string",  "minLength": 1 },
    "precio":  { "type": "number",  "minimum": 0 },
    "enStock": { "type": "boolean" }
  },
  "required": ["nombre", "precio"],
  "additionalProperties": false
}

Fíjate en additionalProperties: false al final. Eso significa que si alguien manda un campo extra que no está en properties —como "descuento" o "color"— la validación también falla. Es opcional, pero en entornos donde quieres control total sobre los datos que recibes es muy útil.

Actividad: ponlo en práctica con WebStorm

Vamos a construir un esquema para un usuario y a ver cómo WebStorm lo usa para avisarte de errores mientras escribes, sin tener que salir del editor.

Crea estos dos archivos en tu proyecto

usuario.schema.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Usuario",
  "type": "object",
  "properties": {
    "nombre": {
      "type": "string",
      "minLength": 1
    },
    "correo": {
      "type": "string",
      "format": "email"
    },
    "fechaAlta": {
      "type": "string",
      "format": "date-time"
    }
  },
  "required": ["nombre", "correo", "fechaAlta"]
}

usuario.json

{
  "nombre": "Alejandro Carmona",
  "correo": "acarmar112@g.educaand.es",
  "fechaAlta": "2024-04-28T12:34:56Z"
}

Asocia el esquema en WebStorm

Ve a Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings. Haz clic en +, ponle un nombre al mapeo, selecciona usuario.schema.json como esquema y asócialo al archivo usuario.json.

A partir de ahí WebStorm sabe qué estructura debe tener ese archivo y te avisa en tiempo real si algo no cuadra.

Ahora rómpelo a propósito

Esto es lo importante. Prueba cada una de estas modificaciones en usuario.json y observa qué pasa:

  1. Borra el campo correo. ¿Qué error aparece?
  2. Escribe el correo como "esto no es un email". ¿Lo detecta?
  3. Pon "fechaAlta": "hoy" en lugar del formato ISO. ¿Falla?
  4. Añade un campo "edad": 25. Si tienes additionalProperties: false en el esquema, ¿lo acepta?

Cuando entiendas exactamente qué valida el esquema y qué no, habrás pillado la idea. Ese es el objetivo.