2.2. JSON Schema

La definición de gramáticas para documentos JSON no está tan madura como para documentos XML. Hay distintas alternativas, de las cuales la más extendida es JSON Schema, que puede utilizarse tanto en la definición de documentos JSON como de documentos YAML.

Dedicaremos el epígrafe a describir cómo se utiliza JSON Schema.

2.2.1. Preliminares

Antes de entrar a definir su sintaxis es conveniente saber algunos aspectos de JSON Schema:

  • Define gramáticas de documentos JSON (o YAML) usando un formato JSON.

  • No tiene tanta madurez como la definición de gramáticas para XML.

  • Existen nueve versiones del borrador de su especificación: la siete primeras nombradas mediante un ordinal (Draft 1, Draft 2, etc.) y las dos últimas con la fecha en que se publicaron: 2019-09 y 2020-12. En consecuencia a fecha de redacción de este documento el borrador más reciente es el 2020-12.

Nota

Obviamente, si se pretende definir una gramática de YAML con JSON Schema, sólo podremos escribir en el YAML aquello que puede escribirse en un JSON. Por ejemplo, no podremos usar undefined o deberemos hacer que todas las claves sean cadenas, a pesar de que YAML no tiene esas limitaciones.

Ver también

Como la especificación es muy árida, ilustraremos aquí los aspectos más básicos, pero para mayor profundización puede echarse mano de la guía oficial Understanding JSON Schema.

2.2.1.1. Ejemplo

Tomemos la versión JSON del ejemplo introductorio:

Casilleros
{
   "centro":  "IES Pepe Botella",
   "profesores": [
      {
         "id": 1,
         "apelativo": "Pepe",
         "nombre": "José",
         "apellidos": "Suárez Lantilla",
         "departamento": "Inglés"
      },
      {
         "id": 13,
         "apelativo": "Cristina",
         "nombre": "María  Cristina",
         "apellidos": "Prieto Monagas",
         "departamento": "Biología y Geología"
      },
      {
         "id": 15,
         "apelativo": "Manolo",
         "nombre": "Manuel",
         "apellidos": "Páez Robledo",
         "departamento": "Matemáticas"

      },
      {
         "id": 17,
         "casillero": [17, 43],
         "apelativo": "Lucía",
         "nombre": "Lucía",
         "apellidos": "Gálvez Ruiz",
         "departamento": "Inglés"
      },
      {
         "id": 28,
         "casillero": [52],
         "apelativo": "Migue",
         "nombre": "Miguel Ángel",
         "apellidos": "Campos Sanchez",
         "departamento": "Historia"
      },
      {
         "id": 81,
         "casillero": [],
         "apelativo": "Vero",
         "nombre": "Verónica",
         "apellidos": "Martín Díaz",
         "departamento": "Biología"
      },
      {
         "id": 86,
         "sustituye": 15,
         "apelativo": "Roberto",
         "nombre": "Roberto",
         "apellidos": "Mínguez Torralbo"
      }
   ]
}

Este JSON se ajusta al siguiente esquema:

Gramática de Casilleros con JSON Schema
{
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "$id": "casilleros.schema.json",
   "type": "object",
   "title": "Casilleros",
   "description": "Asignación de casilleros a profesores",
   "properties": {
      "centro": {
         "type": "string",
         "description": "Nombre del centro de enseñanza"
      },
      "profesores": {
         "type": "array",
         "description": "Relación de profesores",
         "items": {
            "type": "object",
            "properties": {
               "id": { 
                  "type": "integer",
                  "description": "Identificador único del profesor",
                  "minimum": 0

               },
               "sustituye": {
                  "type": "integer",
                  "description": "Identificador del profesor al que sustituye",
                  "minimum": 0
               },
               "casillero": {
                  "type": "array",
                  "items": {
                     "type": "integer",
                     "minimum": 0
                  },
                  "uniqueItems": true
               },
               "apelativo": {
                  "type": "string",
                  "descripcion": "Nombre por el que se le conoce"
               },
               "nombre": {
                  "type": "string",
                  "descripcion": "Nombre de pila"
               },
               "apellidos": {
                  "type": "string",
                  "descripcion": "Apellidos"
               },
               "departamento": {
                  "type": "string",
                  "descripcion": "Departamento didáctico al que pertenece"
               }
            },
            "additionalProperties": false,
            "required": ["id", "nombre", "apellidos"],
            "oneOf": [
               {
                  "required": ["departamento"],
                  "not": { "required": ["sustituye"] }
               },
               {
                  "required": ["sustituye"], 
                  "allOf": [
                     {"not": { "required": ["departamento"] }},
                     {"not": { "required": ["casillero"] }}
                  ]
               }
            ]
         },
         "minItems": 1
      }
   },
   "additionalProperties": false,
   "required": ["centro", "profesores"]
}

Como puede verse, el documento es en sí un documento JSON y tiene un modo algo particular (y nada intuitivo) de expresar una ocurrencia de nodos, que se habría resuelto muy fácilmente con DTD:

(id, apelativo?, nombre, apellidos, ((departamento, casillero?)|sustituye))

2.2.1.2. Validación

Antes de empezar, no obstante, es conveniente saber cómo validar:

  • Validador online, que tiene el inconveniente de que no pueden subirse archivos locales.

  • La orden jsonschema, disponible a través del paquete python3-jsonschema de las distribuciones basadas en Debian.

    $ jsonschema -i casilleros.json casilleros.schema.json
    

    La orden no mostrará salida (y devolverá un 0 al sistema) si el documento respeta la gramática definida en el esquema. En principio, sólo es capaz de validar documentos JSON, pero para validar YAML puede hacerse una conversión previa a JSON con yq.

  • Visual Studio Code, que sirve para validar tanto JSON como YAML.

2.2.2. Sintaxis básica

Ya sabemos que un documento JSON está constituido por nodos, cada uno de los cuales tiene un tipo. Construir el esquema de un documento JSON consiste básicamente en definir los subesquemas que describen cada uno de sus nodos[1].

No nos proponemos profundizar mucho, ya que tienen algo más de complejidad que los DTD.

2.2.2.1. Esquema del nodo raíz

Antes de analizar cómo se define el esquema de cada nodo en general, es preciso indicar las particularidades del nodo raíz. Un nodo raíz tiene este aspecto:

{
   "$schema": "URN-del-esquema-JSON-que-se-usa",
   "$id": "URL-del-documento",

   # Descripción del nodo
}

donde:

$schema

Es la URN de versión de JSON Schema que hemos usado en el archivo. La de la última (2020-12) es la que hemos expresado en el ejemplo.

$id

Es la URL donde se encuentra el archivo con el esquema que estamos definiendo. Tiene utilidad cuando el esquema necesita referir subesquemas que se encuentran definidos en otros archivos. Si no es el caso, puede dejarse sin definir esta propiedad o indicar, simplemente, el nombre del archivo.

Nota

En nuestro ejemplo, no hemos usado una URL porque no estamos escribiendo un esquema público para uso general.

El resto de parejas clave/valor que pueden encontrarse en la raíz describen qué contiene el nodo raíz y, por tanto, serán las claves típicas de un subesquema de nodo. Eso sí, como el nodo raíz de un documento JSON sólo puede ser un mapa o una secuencia, estas claves típicas sólo podrán ser las típicas de un nodo secuencia (array) o de un nodo mapa (map). En el ejemplo, el nodo raíz es un mapa por lo que el esquema inicial podría quedar en principio como:

{
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "$id": "casilleros.schema.json",

   "type": "object",
   "title": "Casilleros",
   "description": "Asignación de casilleros a profesores",
   "properties": {

   }
}

ya que cualquier subesquema de nodo nos permite añadir un título y una descripción y el nodo mapa en particular nos pide al menos indicar cuáles son las propiedades (o sea, las parejas clave/valor) lícitas, para lo cual debemos usar la propiedad properties.

Nota

Dado que en principio no es obligatorio definir todas las propiedades del objeto, un esquema tan simple como éste nos validará el documento JSON. Bien es cierto que sirve de poco, porque la única limitación que introduce es que el nodo raíz es un mapa y no secuencia.

2.2.2.2. Subesquemas de nodo

Cómo se describa un nodo, depende fundamentalmente de su tipo (no es lo mismo describir qué debe cumplir un nodo numérico que un nodo secuencia, por ejemplo). Ahora bien, hay propiedades comunes a todos los nodos.

title

permite indicar un título para el nodo.

description

permite describir qué contiene el nodo de una manera más prolija.

type

indica el tipo de dato (integer, number, string, null, boolean, array u object).

default

permite indicar el valor predeterminado, en caso de que el nodo no aparezca.

enum

define una lista de valores válidos para el nodo, fuera de los cuales debe producirse un error. Por ejemplo:

{
   "type": "integer",
   "description": "Este es un entero con sólo unos pocos valores válidos",
   "enum": [1, 32, 55]
}

es un subesquema que indica que el nodo es un entero con sólo tres posibles valores válidos.

Nota

En realidad, si especificamos cuáles son todos los valores válidos, ya no es necesario especificar el tipo, así que es mejor definir así:

{
   "description": "Este es un entero con sólo unos pocos valores válidos",
   "enum": [1, 32, 55]
}
const

define el único valor válido para el nodo por lo que equivale a un enum cuya lista sólo contenga un valor.

{
   "description": "Este es un entero con sólo unos pocos valores válidos",
   "const": 32
}

Estas propiedades que acabamos de enumerar son aquellas que podemos encontrar sea cual sea el tipo del nodo. Ahora bien, ¿cuáles son específicas?

numeric (que comprende integer y number)

Tiene asociadas propiedades que no requieren demasiada explicación:

minimum/maximum

define el valor mínimo y el máximo respectivamente.

exclusiveMinimum/exclusiveMaximum

Si es true, excluye el valor mínimo y máximo respectivamente como valores validos. En su ausencia, lo son.

multipleOf

fuerza a que el valor sea múltiplo del indicado.

string

El tipo tiene también algunas propiedades particulares:

minLength

define el número mínimo de caracteres que puede contener la cadena.

maxLength

define el número máximo de caracteres que puede contener la cadena.

pattern

define una expresión regular que se usará como patrón para comprobar la validez de la cadena. Las expresiones regulares que define Javascript son prácticamente los patrones ERE y PCRE que pueden consultarse en estos apuntes. Por ejemplo:

{
   "type": "string",
   "description": "Este dato sólo podrá contener cadenas de tres caracteres",
   "pattern": "^...$"
}
format

dispone que la cadena cumple con un formato predefinido determinado. Por ejemplo:

{
   "type": "string",
   "description": "El valor tendrá que ser una fecha con la forma AAAA-MM-DD",
   "format": "date"
}

Ver también

Los formatos predefinidos se encuentran enumerados en la especificación.

Prudencia

La especificación nos advierte de este campo sólo tiene valor informativo y no afecta a la validación, por lo que una cadena que no cumpla con el formato no tiene por qué producir un error en la validación.

null

Dado que sólo hay un valor posible, no tiene ninguna propiedad adicional.

boolean

Tampoco presenta ninguna propiedad adicional.

array

Al no ser la secuencia un valor escalar (a diferencia de todos los anteriores) su definición es algo más compleja. Las propiedades más sencillas de entender son:

minItems/maxItems

es la cantidad mínima (o máxima) de elementos que debe contener la secuencia. Por lo tanto, nos sirve para restringir la longitud de la secuencia.

uniqueItems

fuerza a que no haya dos elementos iguales en la secuencia. Por ejemplo:

{
   "type": "array",
   "uniqueItems": true,
   "minItems": 2
}

forzaría a que la secuencia contuviera al menos dos elementos y que todos fueran distintos entre sí.

contains

indica que la secuencia contiene al menos un elemento con las características del que se indica. Por ejemplo, la secuencia:

{
   "type": "array",
   "contains": {
      "enum": [1, 34, 56]
   }
}

puede tener todos los elementos que se quiera y del tipo que se quiera, pero uno al menos debe ser 1, 34 o 56.

minContains/maxContains

funcionan en conjunción con contains e indican la cantidad mínima o máxima de elementos que deben cumplir con el subesquema incluido en él. Así, en el ejemplo anterior, no se especificó ninguno de estas dos propiedades, por lo que con que haya un elemento que cumpla la prescripción de contains la validación tiene éxito. En cambio, si hacemos:

{
   "type": "array",
   "contains": {
      "enum": [1, 34, 56]
   },
   "minContains": 4
}

tendrá que haber al menos cuatro elementos que cumplan con el esquema de contains.

items

indica el esquema que deben cumplir todos los elementos que constituyen la secuencia. Por tanto, definida así:

{
   "type": "array",
   "items": {
      "type": "integer",
   }
}

la secuencia sólo podrá contener enteros. Si items tiene el valor false, no podrá contener elementos:

{
   "type": "array",
   "description": "Esto valida una secuencia vacía",
   "items": false,
}
prefixItems

tiene utilidad cuando a diferencia del caso anterior, cada elemento de la secuencia tiene un esquema diferente:

{
   "type": "array"
   "prefixItems": [
      {"type": "integer"},
      {"type": "string"}
   ]
}

En este ejemplo, el primer elemento debe ser un entero y el segundo una cadena, aunque la validación también tendrá éxito cuando haya más de dos elementos (y éstos no están sujetos a ninguna condición) o incluso cuando haya menos. Todos estas secuencias son válidas:

[5, "x"]
[6, "y", true. null]
[5]
[]

Nota

Nótese que podríamos establecer que los elementos fueran exactamente 2 añadiendo al esquema minItems y maxItems.

La razón del nombre de la propiedad (prefixItems) es que esta propiedad define el esquema de los elementos anteriores a los definidos por items. Por eso, esta definición:

{
   "type": "array"
   "prefixItems": [
      {"type": "integer"},
      {"type": "string"}
   ],
   "items": {
      "const": 32
   }
}

obligaría a que a partir del tercer elemento (si los hubiere, todos fueran el número 32).

object

El subesquema que describe un mapa es el que entraña más dificultad. La propiedad fundamental es:

properties

que permite describir las propiedades que pueden encontrarse en el objeto. Por ejemplo, un mapa con este aspecto:

{
   "nombre": "Pedro Martínez Álvarez",
   "edad": 32,
   "casado": true,
   "hijos": [
      "Felipe",
      "Sonsoles"
   ]
}

podríamos definirlo así:

{
   "type": "object",
   "properties": {
      "nombre": { "type": "string"},
      "edad": { "type": "integer", "exclusiveMinimum": 0},
      "casado": { "type": "boolean", "default": false},
      "hijos": {
         "type": "array",
         "items": { "type": "string" }
      }
   }
}

Como puede apreciarse la claves de properties definen las claves del propio mapa a definir y los valores el subesquema que define el nodo valor. De los nodos que representan las claves, no hay en principio mucho que definir, puesto que deben ser cadenas, así que no hay esquema para ellos.

La definición de properties, sin embargo, no obliga a que las únicas claves posibles sean las definidas ni a que aparezcan todas. Para ello, podemos añadir otras propiedades:

required

Lista las propiedades que son obligatorias. Por ejemplo:

{
   "type": "object",
   "properties": {
      "nombre": { "type": "string"},
      "edad": { "type": "integer", "exclusiveMinimum": 0},
      "casado": { "type": "boolean", "default": false},
      "hijos": {
         "type": "array",
         "items": { "type": "string" }
      }
   },
   "required": ["nombre"]
}

Una definición obliga a que el mapa siempre presente la propiedad «nombre».

additionalProperties

Define el esquema que deben cumplir las propiedades que no han sido listadas en properties (ni patternProperties). Por ejemplo:

{
   "type": "object",
   "properties": {
      "nombre": { "type": "string"},
      "edad": { "type": "integer", "exclusiveMinimum": 0},
      "casado": { "type": "boolean", "default": false},
      "hijos": {
         "type": "array",
         "items": { "type": "string" }
      }
   },
   "required": ["nombre"],
   "additionalProperties": {"type": "string"}
}

provocaría que los valores de las propiedades no definidas expresamente sólo pudieran ser cadenas. Si el valor, en vez de un subesquema, es false, no se permitirá ninguna propiedad adicional.

patternProperties

Funciona como properties, pero en vez de definir propiedades con un nombre concreto, define propiedades cuya clave cumple con un patrón (una expresión regular). Por ejemplo, este esquema:

{
   "type": "object",
   "properties": {
      "concreta":  {"type": "integer"},
      "tambienconcreta": {"type": "number"}
   }
   "patternProperties": {
      "^s-": {"type": "string"},
      "^b-": {"type": "boolean"}
   },
   "additionalProperties": false
}

provoca que las claves válidas sean «concreta», «tambienconcreta», cualquier clave que empiece por «s-» y cualquier clave que empiece por «b-».

unevaluatedProperties

no entraremos a tratarla extensamente, pero básicamente viene a complementar a additionalProperties, la cual sólo es afectada por las propiedades enumeradas dentro de properties o a las que se ajustan en los patrones incluidos en patternProperties. En cambio, unevaluatedProperties es capaz de comprobar propiedades que se encuentran dentro de esquemas condicionales o en esquemas referenciados. Por ejemplo:

{
   "type": "object",
   "properties": {
      "nombre": { "type": "string"}
   },
   "oneOf": [
      {
         "properties": { "edad": { "type": "integer", "exclusiveMinimum": 0} },
         "not": { "required": ["nacimiento"] }
      },
      {
         "properties": { "nacimiento": { "type": "string", "format": "date"} },
         "not": { "required": ["edad"] }
      }
   ],
   "required": ["nombre"],
   "unevaluatedProperties": false
}

En este esquema, edad y nacimiento están definidos dentro de un esquema condicional (con oneOf)[2], por lo que si usaramos additionalProperties, el documento JSON sería inválido si incluyéramos edad o nacimiento.

Además de las anteriores, hay otras también limitantes:

minProperties/maxProperties

define la cantidad mínima o máxima de propiedades que puede presentar el objeto.

dependentRequired

define qué propiedades deben existir en el objeto para que otra pueda aparecer. Por ejemplo, imaginemos que queremos que casado aparezca sólo si edad se incluyó en el mapa (que en principio no es obligatoria). En ese caso, habría que definir el esquema así:

{
   "type": "object",
   "properties": {
      "nombre": { "type": "string"},
      "edad": { "type": "integer", "exclusiveMinimum": 0},
      "casado": { "type": "boolean", "default": false},
      "hijos": {
         "type": "array",
         "items": { "type": "string" }
      }
   },
   "required": ["nombre"],
   "additionalProperties": false,
   "dependentRequired": {
      "casado": [ "edad" ]
   }
}

Es decir, cada una de las claves es la propiedad que presenta dependencias y para cada una de ella, se listan las propiedades requeridas.

dependentSchemas

permite definir con más precisión la dependencia, ya que no se limita a comprobar la existencia, sino también a comprobar si los valores se ajustan a un determinado esquema. Por ejemplo, legalmente no es posible casarse hasta los 16 años, de modo que tal vez no nos interese sólo asegurarnos de que está expresada la edad, sino de que ésta es de al menos 16 años:

{
   "type": "object",
   "properties": {
      "nombre": { "type": "string"},
      "edad": { "type": "integer", "exclusiveMinimum": 0},
      "casado": { "type": "boolean", "default": false},
      "hijos": {
         "type": "array",
         "items": { "type": "string" }
      }
   },
   "required": ["nombre"],
   "additionalProperties": false,
   "dependentSchemas": {
      "casado": {
         "properties": {
            "edad": { "minimum": 16 }
         },
         "required": ["edad"]
      }
   }
}

2.2.3. Referencias

Por la naturaleza del formato JSON, la definición de los subesquemas de los nodos se va anidando a otros subesquemas en los que se han definido secuencias o mapas. Ilustrémoslo con un JSON muy sencillo reciclado de ejemplos anteriores:

[
   {
      "nombre": "Pedro Martínez Álvarez",
      "edad": 32,
      "casado": true
   },
   {
      "nombre": "Marta Martínez Campoy",
      "edad": 12
   }
]

Su esquema, a estas alturas, no debería entrañar ninguna dificultad:

{
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "$id": "gente.schema.json",

   "type": "array",
   "title": "Ejemplo de referencia",
   "description": "Un porrón de personas dentro de una secuencia",
   "items": {
      "type": "object",
      "properties": {
         "nombre": {
            "type": "string",
            "description": "Nombre completo de la persona"
         },
         "edad": {
            "type": "integer",
            "minimum": 0
         },
         "casado": {
            "type": "boolean",
            "default": false
         }
      },
      "additionalProperties": false,
      "required": ["nombre"]
   },
   "uniqueItems": true
}

Como puede apreciarse, el subesquema que describe a la persona está anidado dentro de la definición de la secuencia, concretamente, en su propiedad properties. Como el esquema completo es sencillo, no hay problemas. Sin embargo, en esquemas más complejos con varios niveles de anidación, podemos encontrar dificultades para seguir las definiciones. Por ese motivo, JSON Schema permite incluir referencias a un subesquema y escribir éste separadamente. También son útiles las referencias cuando un subesquema se repite en varias partes del documento.

Así pues, partamos la definición en dos archivos:

{
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "$id": "persona.schema.json",

   "type": "object",
   "properties": {
      "nombre": {
         "type": "string",
         "description": "Nombre completo de la persona"
      },
      "edad": {
         "type": "integer",
         "minimum": 0
      },
      "casado": {
         "type": "boolean",
         "default": false
      }
   },
   "additionalProperties": false,
   "required": ["nombre"]
}

y persona.schema.json:

{
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "$id": "gente.schema.json",

   "type": "array",
   "title": "Ejemplo de referencia",
   "description": "Un porrón de personas dentro de una secuencia",
   "items": {
      "$ref": "URL-donde-puedo-encontrar-persona.schema.json"
   },
   "uniqueItems": true
}

Como vemos, gente.schema.json sustituye el subesquema de la persona, por una simple referencia al archivo persona.schema.json a través de la propiedad $ref. La única dificultad es saber cómo funcionan estas URLs.

Podemos usar una URL absoluta (p.e. https://example.net/schemas/persona.schema.json), en cuyo caso no habrá problemas, pero si queremos usar una URL relativa, es necesario profundizar más.

El primer concepto a introducir es el de URL de recuperación, que es la URL de la que toma el validador el archivo con el esquema. Por ejemplo, si ha tomado el archivo de https://example.net/schemas/gente.schema.json, esa será la URL de recuperación. A partir de ella se define la URL base, que es la URL descontada la parte correspondiente al archivo. En este caso, la URL base es https://example.net/schemas/. Sin embargo, la URL de recuperación puede no estar definida y, por tanto, tampoco la URL base. Por ese motivo, existe la propiedad $id en el nodo raíz. Si existe, es su valor el que se toma como referencia para calcular la URL base.

Así pues, tendríamos varias opciones para escribir la URL de persona.schema.json (suponiendo que estuviera ubicando en el mismo lugar que gente.schema.json):

  • Absoluta: https://example.net/schemas/persona.schema.json.

  • Absoluta sin máquina ni protocolo: /schemas/persona.schema.json.

  • Relativa: persona.schema.json.

    Prudencia

    Con la orden jsonschema sugerida, se debe anteponer file: cuando se usan rutas relativas y los archivos son locales. Por tanto, file:persona.schema.json.

Además de todo lo ya referido, es posible hacer referencia a subesquemas contenidos dentro de un esquema mayor. Por ejemplo, persona.schema.json#/properties/nombre referiría el subesquema:

{
   "type": "string",
   "description": "Nombre completo de la persona"
}

Esto da pie a recuperar subesquemas de otros archivos, pero también subesquemas definidos en otra parte del archivo. Con este fin existe la propiedad

$defs

Contiene subesquemas con nombre a los que puede hacerse referencia:

Esquema con referencia interna
{
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "$id": "gente.schema.json",

   "type": "array",
   "title": "Ejemplo de referencia",
   "description": "Un porrón de personas dentro de una secuencia",
   "items": { "$ref": "#/$defs/persona" },
   "uniqueItems": true,

   "$defs": {
      "persona": {
         "type": "object",
         "properties": {
            "nombre": {
               "type": "string",
               "description": "Nombre completo de la persona"
            },
            "edad": {
               "type": "integer",
               "minimum": 0
            },
            "casado": {
               "type": "boolean",
               "default": false
            }
         },
         "additionalProperties": false,
         "required": ["nombre"]
      }
   }
}

2.2.4. Esquemas condicionales

Un esquema condicional es aquel cuyas características dependen de que se cumplan uno o varios requisitos. Por ejemplo, que una propiedad sea obligatoria sólo si se presenta otra. Ya hemos visto dos propiedades que crean esquemas condicionales: dependentRequired y dependentSchemas. Hay, si embargo, otros modos de crearlos:

if/then/else

Estas propiedades funcionan de forma semejante a como lo hace la estructura condicional en los lenguajes de programación:

  1. El valor if es un esquema que se evalúa.

  2. Si resulta verdadero, se evalúa el esquema de then, que debe resultar verdadero.

  3. Si resulta falso, se evalúa en caso de existir el esquema de else, que debe resultar verdadero.

Por ejemplo, el mismo caso que resolvimos con dependentSchemas, podemos resolverlo así:

{
   "type": "object",
   "properties": {
      "nombre": { "type": "string"},
      "edad": { "type": "integer", "exclusiveMinimum": 0},
      "casado": { "type": "boolean", "default": false},
      "hijos": {
         "type": "array",
         "items": { "type": "string" }
      }
   },
   "required": ["nombre"],
   "additionalProperties": false,
   "if": {
      "required": ["casado"]
   },
   "then": {
      "properties": {
         "edad": {"minimum": 16}
      },
      "required": ["edad"]
   }
}

Nota

Puede probar a reescribir con estas propiedades los requisitos de presencia de casillero, departamento y sustituye del ejemplo inicial.

oneOf

El esquema es válido sólo si uno de los propuestos en la lista es válido. Por ejemplo:

{
   "oneOf": [
      { "type": "integer", "maximum": 10 },
      { "type": "boolean" }
   ]
}

En este caso, el valor puede ser un entero hasta 10 o un valor lógico. La alternativa no tiene por qué ser únicamente sobre tipos:

{
   "oneOf": [
      { "type": "integer", "maximum": 10 },
      { "const": false }
   ]
}

Y ni siquiera tiene abarcar toda la definición del subesquema:

{
   "type": "integer",
   "oneOf":  [
      {"maximum": 10},
      {"minimum": 20}
   ]
}

En este caso, cumplirían con el esquema todos los enteros, excepto aquellos comprendidos entre 11 y 19.

anyOf

La diferencia respecto a oneOf es que basta con que se cumpla uno, pero no necesariamente uno. Por ejemplo:

{
   "type": "integer",
   "anyOf":  [
      {"maximum": 10, "minimum": 0},
      {"multipleOf": 5}
   ]
}

En este caso, serán válidos todos los enteros hasta 10 y cualquier múltiplo de 5. Sin embargo, si hubiéramos construido la combinación con oneOf, 0, 5 y 10 incumplirían el esquema, porque cumplen ambas condiciones y sólo puede cumplirse una.

allOf

Como las dos anteriores, pero obliga a que se cumplan todos los esquemas incluidos en la lista. Por tanto:

{
   "type": "integer",
   "allOf":  [
      {"maximum": 10, "minimum": 0},
      {"multipleOf": 5}
   ]
}

sólo sería válido para 0, 5 y 10.

not

Invierte la validez del esquema, es decir, el valor será válido, si el esquema negado es inválido para el valor. Por ejemplo:

{
   "not": {
      "type": "integer"
   }
}

Cualquier valor será válido siempre que no sea un entero.

2.2.5. Ejercicios resueltos

Tomando las versiones JSON (o YAML) de los dos ejercicios ya resueltos, podemos para el primero definir así su gramática:

recetas.schema.json
{
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "$id": "facturas.schema.json",

    "title": "Recetas de cocina",

   "type": "array",
   "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["nombre", "tiempo", "ingredientes"],
        "properties": {
            "nombre": {"type": "string"},
            "tiempo": {
                "description": "Tiempo de elaboración de la receta",
                "type": "number",
                "minimumExclusive": 0
            },
            "ingredientes": {
                "type": "array",
                "minItems": 1,
                "items": {"$ref": "#/$defs/ingrediente"}
            }
        }
   },
   "$defs": {
        "ingrediente": {
            "description": "Definición del ingrediente en una receta",
            "type": "object",
            "properties": {
                "nombre": {"type": "string"},
                "unidad": {"enum": ["pieza", "gramo", "ml", "cucharada"]},
                "cantidad": {"type": "number"}
            },
            "additionalProperties": false,
            "required": ["nombre", "unidad", "cantidad"]
        }
   }
}

El segundo podemos definirlo con otra gramática que referencie la primera:

restaurantes.schema.json
{
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "$id": "facturas.schema.json",

   "title": "Cadena de restaurantes",

   "type": "object",
   "required": ["recetas", "cadena"],
   "additionalProperties": false,
   "properties": {
        "recetas": {
            "description": "Recetas tal como se definen en otro esquema",
            "$ref": "recetas.schema.json/"
        },
        "cadena": {
            "type": "array",
            "items": {"$ref": "#/$defs/restaurante"}
        }
   },
   "$defs": {
        "restaurante": {
            "description": "Restaurante de la cadena",
            "type": "object",
            "additionalProperties": false,
            "required": ["nombre", "tlfo", "direccion", "carta"],
            "properties": {
                "nombre": {"type": "string"},
                "tlfo": {"type": "number"},
                "direccion": {"$ref": "#/$defs/direccion"},
                "carta": {
                    "type": "array",
                    "items": {"$ref": "#/$defs/plato"},
                    "minItems": 1
                }
            }
        },
        "direccion": {
            "description": "Dirección postal",
            "type": "object",
            "additionalProperties": false,
            "required": ["via", "municipio", "cp"],
            "properties": {
                "via": {"type": "string"},
                "municipio": {"type": "string"},
                "cp": {"type": "integer"}
            }
        },
        "plato": {
            "description": "Plato servido en un resturante",
            "additionalProperties": false,
            "required": ["plato", "formato"],
            "properties": {
                "plato": {
                    "description": "Nombre de la receta",
                    "type": "string"
                },
                "formato": {
                    "description": "Formato en que se sirve el plato",
                    "type": "array",
                    "items": {
                        "enum": ["tapa", "racion", "media"]
                    }
                }
            }
        }
   }
}

2.2.6. Ejercicios propuestos

Tome de la relación de ejercicios propuestos para la primera unidad las soluciones JSON (o YAML, ambas deberían ser equivalentes) que diseñó a partir del enunciado 5, y escriba para ellas la gramática con JSON Schemas.

Asegúrese para cada enunciado de validar ambas soluciones (JSON y YAML) con el mismo esquema JSON.

Nota

Quizás el profesor prefiera proporcionar sus propias soluciones para evitar que el alumno defina la gramática de una solución incompleta o deficiente.

Notas al pie