Advertisement
  1. Code
  2. JSON

Validación de datos con JSON-Schema, parte 2

Scroll to top
Read Time: 36 min
This post is part of a series called Validating Data With JSON-Schema.
Validating Data With JSON-Schema: Part 1

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

En la primera parte de este tutorial, aprendió a crear esquemas bastante avanzados utilizando todas las palabras clave de validación disponibles. Muchos ejemplos del mundo real de datos JSON son más complejos que nuestro ejemplo de usuario. Un intento de poner todos los requisitos a tales datos en un archivo puede conducir a un esquema muy grande que también puede tener una gran cantidad de duplicación.

Cómo estructurar sus esquemas

El estándar de esquema JSON le permite romper esquemas en varias partes. Echemos un vistazo al ejemplo de los datos para la navegación del sitio de noticias:

1
{
2
  "level": 1,
3
  "parent_id": null,
4
  "visitors": "all",
5
  "color": "white",
6
  "pages": [
7
    {
8
      "page_id": 1,
9
      "short_name": "home",
10
      "display_name": "Home",
11
      "url": "/home",
12
      "navigation": {
13
        "level": 2,
14
        "parent_id": 1,
15
        "color": "blue",
16
        "pages": [
17
          {
18
            "page_id": 11,
19
            "short_name": "headlines",
20
            "display_name": "Latest headlines",
21
            "url": "/home/latest",
22
            "navigation": {
23
              "level": 3,
24
              "parent_id": 11,
25
              "color": "white",
26
              "pages": [
27
                {
28
                  "page_id": 111,
29
                  "short_name": "latest_all",
30
                  "display_name": "All",
31
                  "url": "/home/latest"
32
                },
33
                ...
34
              ]
35
            }
36
          },
37
          {
38
            "page_id": 12,
39
            "short_name": "events",
40
            "display_name": "Events",
41
            "url": "/home/events"
42
          }
43
        ]
44
      }
45
    },
46
    ...
47
  ]
48
}

La estructura de navegación anterior es algo similar a la que se puede ver en el sitio web http://dailymail.co.uk. Puede ver un ejemplo más completo en el repositorio de GitHub.

La estructura de datos es compleja y recursiva, pero los esquemas que describen estos datos son bastante simples:

navigation.json:

1
{
2
  "$schema": "http://json-schema.org/draft-04/schema#",
3
  "id": "http://mynet.com/schemas/navigation.json#",
4
  "title": "Navigation",
5
  "definitions": {
6
    "positiveIntOrNull": { "type": ["null", "integer"], "minimum": 1 }
7
  },
8
  "type": "object",
9
  "additionalProperties": false,
10
  "required": [ "level", "parent_id", "color", "pages" ],
11
  "properties": {
12
    "level":     { "$ref": "defs.json#/definitions/positiveInteger" },
13
    "parent_id": { "$ref": "#/definitions/positiveIntOrNull" },
14
    "visitors":  { "enum": [ "all", "subscribers", "age18" ] },
15
    "color":     { "$ref": "defs.json#/definitions/color" },
16
    "pages":     {
17
      "type": "array",
18
      "items": { "$ref": "page.json#" }
19
    }
20
  }
21
}

page.json:

1
{
2
  "$schema": "http://json-schema.org/draft-04/schema#",
3
  "id": "http://mynet.com/schemas/page.json#",
4
  "title": "Page",
5
  "type": "object",
6
  "additionalProperties": false,
7
  "required": [ "page_id", "short_name", "display_name", "path" ],
8
  "properties": {
9
    "page_id":      { "$ref": "defs.json#/definitions/positiveInteger" },
10
    "short_name":   { "type": "string", "pattern": "^[a-z_]+$" },
11
    "display_name": { "type": "string", "minLength": 1 },
12
    "path":         { "type": "string", "pattern": "^(?:/[a-z_\\-]+)+$" },
13
    "color":        { "$ref": "defs.json#/definitions/color" },
14
    "navigation":   { "$ref": "navigation.json#" }
15
  }
16
}

defs.json:

1
{
2
  "$schema": "http://json-schema.org/draft-04/schema#",
3
  "id": "http://mynet.com/schemas/defs.json#",
4
  "title": "Definitions",  
5
  "definitions": {
6
    "positiveInteger": { "type": "integer", "minimum": 1 },
7
    "color": {
8
      "anyOf": [
9
        { "enum": [ "red", "green", "blue", "white" ] },
10
        { "type": "string", "pattern": "^#(?:(?:[0-9a-fA-F]{1,2})){3}$" }
11
      ]
12
    }
13
  }
14
}

Eche un vistazo a los esquemas anteriores y los datos de navegación que describen (que son válidos de acuerdo con el esquema navigation.json). Lo más importante es notar que el esquema navigation.json hace referencia al schema page.json que a su vez hace referencia al primero.

El código JavaScript para validar el registro de usuario en el esquema podría ser:

1
var Ajv = require('ajv');
2
var ajv = Ajv({
3
  allErrors: true,
4
  schemas: [
5
    require('./navigation.json'),
6
    require('./page.json'),
7
    require('./defs.json')
8
  ]
9
});
10
11
var validate = ajv.getSchema("http://mynet.com/schemas/navigation.json#");
12
var valid = validate(navigationData);
13
if (!valid) console.log(validate.errors);

Todos los ejemplos de código están disponibles en el Repositorio de GitHub.

Ajv, el validador utilizado en el ejemplo, es el validador JSON-Schema más rápido para JavaScript. Lo creé, así que voy a usarlo en este tutorial. Vamos a ver cómo se compara con otros validadores en el final para que pueda elegir el adecuado para usted.

Tareas

Consulte la parte 1 del tutorial para obtener instrucciones sobre cómo instalar el repositorio con las tareas y probar sus respuestas.

Referencias entre esquemas con la palabra clave "$ref"

El estándar JSON-Schema le permite reutilizar las partes repetidas de esquemas usando referencias con la palabra clave "$ref". Como puede ver en el ejemplo de navegación, puede hacer referencia al esquema que se encuentra:

  • en otro archivo: use el URI del esquema que se define en su propiedad "id"
  • en cualquier parte de otro archivo: añada el puntero JSON a la referencia de esquema
  • en cualquier parte del esquema actual: agregue el puntero JSON a "#"

También puede referirse a todo el esquema actual usando "$ref" igual a "#" - le permite crear esquemas recursivos que se refieren a sí mismos.

Así que en nuestro ejemplo, el esquema en navigation.json se refiere a:

  • el esquema page.json 
  • definiciones definitions en el esquema  defs.json
  • la definición positiveIntOrNull en el mismo esquema

El esquema en page.json se refiere a:

  • volver al esquema navigation.json
  • también a definiciones definitions en el archivo defs.json

La norma requiere que la "$ref" sea la única propiedad del objeto, por lo que si desea aplicar un esquema de referencia además de otro esquema, debe usar la palabra clave "allOf".

Tarea 1

Refactorice el esquema de usuario de la parte 1 del tutorial usando referencias. Separe el esquema en dos archivos: user.json y connection.json.

Ponga sus esquemas en los archivos part2/task1/user.json y part2/task1/connection.json y ejecute node part2/task1/validate para verificar si sus esquemas son correctos.

JSON-Pointer

JSON-pointer es un estándar que define las rutas de acceso a las partes de los archivos JSON. El estándar se describe en RFC6901.

Este camino consiste en segmentos (que pueden ser cualquier cadena) conectados con el carácter "/". Si el segmento contiene caracteres "~" o "/", deben reemplazarse por "~ 0" y "~ 1". Cada segmento significa la propiedad o el índice en los datos JSON.

Si miras el ejemplo de navegación, el "$ ref" que define la propiedad color es "defs.json#/definitions/color", donde "defs.json #" es el URI del esquema y "/definitions/color" es el JSON. Señala el color de la propiedad dentro de las definiciones definitions de propiedad.

La convención es poner todas las partes del esquema que se usan en refs dentro de la propiedad definitions del esquema (como se puede ver en el ejemplo). Aunque el estándar de esquema JSON se reserva la palabra clave definiciones definitions para este propósito, no es necesario que coloque sus subesquemas allí. JSON-pointer le permite referirse a cualquier parte del archivo JSON.

Cuando se usan punteros JSON en URIs, todos los caracteres que no son válidos en URIs deben escaparse (en JavaScript se puede usar la función global encodeURIComponent).

Los punteros JSON se pueden utilizar no sólo en los esquemas JSON. Pueden ser usados ​​para representar la trayectoria en datos de JSON a cualquier característica o artículo. Puede utilizar la biblioteca json-pointer para acceder a objetos con punteros JSON.

Tarea 2

El archivo JSON a continuación describe la estructura de carpetas y archivos (los nombres de las carpetas comienzan con "/"):

1
{
2
  "/": {
3
    "/documents": {
4
      "my_story~.rtf": {
5
        "type": "document",
6
        "application": ["Word", "TextEdit"],
7
        "size": 30476
8
      },
9
      ...
10
    },
11
    "/system": {
12
      "/applications": {
13
        "Word": {
14
          "type": "executable",
15
          "size": 1725058307
16
        },
17
        ...
18
      }
19
    }
20
  }
21
}

¿Qué son los punteros JSON que apuntan a:

  • el tamaño de la aplicación "Word"
  • el tamaño del documento "my_story~.rtf" ,
  • el nombre de la segunda aplicación que puede abrir el documento "my_story ~ .rtf" ?

Coloque sus respuestas en part2/task2/json_pointers.json y ejecute node part2/task2/validate para comprobarlos.

ID de esquema

Los esquemas usualmente tienen una propiedad "id" de nivel superior que tiene el URI del esquema. Cuando "$ref" se utiliza en un esquema, su valor se trata como un URI que se resuelve relativamente al esquema "id".

La resolución funciona de la misma manera que el navegador resuelve los URI que no son absolutos: se resuelven con respecto al URI del esquema que se encuentra en su propiedad "id". Si "$ref" es un nombre de archivo, reemplaza el nombre de archivo en el "id". En el ejemplo de navegación, el ID de esquema de navegación es "http://mynet.com/schemas/navigation.json#", por lo que cuando se resuelva la referencia "page.json#", el URI completo del esquema de página se convierte en "http: //mynet.com/schemas/page.json#" (que es el "id"del esquema page.json).

Si el "$ref" del esquema de página era una ruta, p. "/page.json", entonces se habría resuelto como "http://mynet.com/page.json#". Y "/folder/page.json" se habría resuelto como "http://mynet.com/folder/page.json#".

Si "$ref" comienza desde el carácter "#", se trata como un fragmento hash y se añade a la ruta en el "id" (reemplazando el fragmento hash). En el ejemplo de navegación, la referencia "defs.json#/definitions/color" se resuelve como "http://mynet.com/schemas/defs.json#/definitions/color" donde "http://mynet.com/schemas/defs.json#" es el ID del esquema de definiciones y "/definitions/color" es tratado como un puntero JSON dentro de él.

Si "$ref" era un URI completo con un nombre de dominio diferente, de la misma manera que los enlaces funcionan en el navegador, se habría resuelto como el mismo URI completo.

ID de esquema interno

El estándar de esquema JSON le permite usar "id" dentro del esquema para identificar estos subesquemas y también para cambiar el URI de base en relación a qué referencias internas serán resueltas-se llama "cambiar el alcance de resolución". Ésa es probablemente una de las partes más confusas del estándar, y ése es porqué no es muy de uso general.

No recomendaría usar IDs internos excesivos, con una excepción a continuación, por dos razones:

  • Muy pocos validadores siguen sistemáticamente el estándar y resuelven correctamente las referencias cuando se utilizan identificadores internos (Ajv sigue completamente el estándar aquí).
  • Los esquemas se vuelven más difíciles de entender.

Aún veremos cómo funciona porque puede encontrar esquemas que usan ID internos y hay casos en que al usarlos ayuda con la estructuración de sus esquemas.

En primer lugar, echemos un vistazo a nuestro ejemplo de navegación. La mayor parte de las referencias están en el objeto definitions y que hace las referencias bastante largas. Hay una manera de acortarlos añadiendo ID a las definiciones. Este es el esquema defs.json actualizado:

1
{
2
  "$schema": "http://json-schema.org/draft-04/schema#",
3
  "id": "http://mynet.com/schemas/defs.json#",
4
  "title": "Definitions",  
5
  "definitions": {
6
    "positiveInteger": { "id": "#positiveInteger", "type": "integer", "minimum": 1 },
7
    "color": {
8
      "id": "#color",
9
      "anyOf": [
10
        { "enum": [ "red", "green", "blue", "white" ] },
11
        { "type": "string", "pattern": "^#(?:(?:[0-9a-fA-F]{1,2})){3}$" }
12
      ]
13
    }
14
  }
15
}

Ahora, en lugar de las referencias "defs.json#/definitions/positiveInteger" y "defs.json#/definitions/color" que se utilizan en navegación y esquemas de página, puede usar referencias más cortas: "defs.json#positiveInteger" y "defs.json#color" Es un uso muy común de los ID internos, ya que le permite hacer sus referencias más cortas y más legibles. Tenga en cuenta que si bien este caso simple será manejado correctamente por la mayoría de los validadores de esquema JSON, es posible que algunos de ellos no lo admitan.

Veamos un ejemplo más complejo con IDs. Aquí está el ejemplo del esquema JSON:

1
{
2
  "id": "http://x.y.z/rootschema.json#",
3
  "definitions": {
4
    "bar": { "id": "#bar", "type": "string" }
5
  },
6
  "subschema": {
7
    "id": "http://somewhere.else/completely.json#",
8
    "definitions": {
9
      "bar": { "id": "#bar", "type": "integer" }
10
    },
11
    "type": "object",
12
    "properties": {
13
      "foo": { "$ref": "#bar" }
14
    }
15
  },
16
  "type": "object",
17
  "properties": {
18
    "bar": { "$ref": "#/subschema" },
19
    "baz": { "$ref": "#/subschema/properties/foo" },
20
    "bax": { "$ref": "http://somewhere.else/completely.json#bar" }
21
  }
22
}

En muy pocas líneas, se hizo muy confuso. Echa un vistazo al ejemplo y trata de averiguar qué propiedad debe ser una cadena y cuál es un entero.

El esquema define un objeto con propiedades bar, baz y bax. La propiedad bar debe ser un objeto que sea válido de acuerdo con el subesquema, que requiere que su propiedad foo sea válida de acuerdo con la referencia "bar". Debido a que el subschema tiene su propio "id", el URI completo para la referencia será "http://somewhere.else/completely.json#bar", por lo que debe ser un entero.

Ahora mira las propiedades baz y bax. Las referencias para ellos se escriben de una manera diferente, pero apuntan a la misma referencia "http://somewhere.else/completely.json#bar" y ambos deben ser enteros. Aunque la propiedad baz apunta directamente al esquema {"$ref": "#bar"}, debe resolverse en relación con el ID del subesquema porque está dentro de él. Así que el objeto de abajo es válido de acuerdo con este esquema:

1
{
2
  "bar": { "foo": 1 },
3
  "baz": 2,
4
  "bax": 3
5
}

Muchos validadores de esquema JSON no lo manejarán correctamente y es por eso que los identificadores que cambian el ámbito de resolución deben utilizarse con precaución.

Tarea 3

Resolver este rompecabezas le ayudará a entender mejor cómo funcionan las referencias y el alcance cambiante de la resolución. Su esquema es:

1
{
2
  "id": "http://x.y.z/rootschema.json#",
3
  "title": "Task 3",
4
  "description": "Schema with references - create a valid data",
5
  "definitions": {
6
    "my_data": { "id": "#my_data", "type": "integer" }
7
  },
8
  "schema1": {
9
    "id": "#foo",
10
    "allOf": [ { "$ref": "#my_data" } ]
11
  },
12
  "schema2": {
13
    "id": "otherschema.json",
14
    "definitions": {
15
      "my_data": { "id": "#my_data", "type": "string" }
16
    },
17
    "nested": {
18
      "id": "#bar",
19
      "allOf": [ { "$ref": "#my_data" } ]
20
    },
21
    "alsonested": {
22
      "id": "t/inner.json#baz",
23
      "definitions": {
24
        "my_data": { "id": "#my_data", "type": "boolean" }
25
      },
26
      "allOf": [ { "$ref": "#my_data" } ]
27
    }
28
  },
29
  "schema3": {
30
    "id": "http://somewhere.else/completely#",
31
    "definitions": {
32
      "my_data": { "id": "#my_data", "type": "null" }
33
    },
34
    "allOf": [ { "$ref": "#my_data" } ]
35
  },
36
  "type": "object",
37
  "properties": {
38
    "foo": { "$ref": "#foo" },
39
    "bar": { "$ref": "otherschema.json#bar" },
40
    "baz": { "$ref": "t/inner.json#baz" },
41
    "bax": { "$ref": "http://somewhere.else/completely#" },
42
    "quux": { "$ref": "#/schema3/allOf/0" }
43
  },
44
  "required": [ "foo", "bar", "baz", "bax", "quux" ]
45
}

Cree un objeto que sea válido de acuerdo con este esquema.

Ponga su respuesta en part2/task3/valid_data.json y ejecute node part2/task3/validate para comprobarlo.

Cargando esquemas referenciados

Hasta ahora estábamos viendo esquemas diferentes refiriéndonos unos a otros sin prestar atención a cómo se cargan al validador.

Un enfoque es tener todos los esquemas conectados precargados como lo hicimos en el ejemplo de navegación anterior. Pero hay situaciones en las que no es práctico o imposible -por ejemplo, si el esquema que necesitas usar es suministrado por otra aplicación o si no sabes de antemano todos los esquemas posibles que pueden ser necesarios.

En tales casos, el validador podría cargar esquemas referenciados en el momento en que los datos son validados. Pero eso haría lento el proceso de validación. Ajv le permite compilar un esquema en una función de validación de manera asíncrona cargando los esquemas referenciados faltantes en el proceso. La validación misma seguiría siendo sincrónica y rápida.

Por ejemplo, si los esquemas de navegación estuvieran disponibles para descargar desde los URI en sus ID, el código para validar los datos en el esquema de navegación podría ser:

1
var Ajv = require('ajv');
2
var request = require('request');
3
var ajv = Ajv({ allErrors: true, loadSchema: loadSchema });
4
5
var _validateNav; // validation function will be cached here once loaded and compiled

6
7
function validateNavigation(data, callback) {
8
  if (_validateNav) setTimeout(_validate);
9
  loadSchema('http://mynet.com/schemas/navigation.json', function(err, schema) {
10
    if (err) return callback(err);
11
    ajv.compileAsync(schema, function(err, v) {
12
      if (err) callback(err);
13
      else {
14
        _validateNav = v;
15
        _validate();
16
      }
17
    });
18
  });
19
20
  function _validate() {
21
    var valid = _validateNav(data);
22
    callback(null, { valid: valid, errors: _validateNav.errors });
23
  }
24
}
25
26
27
function loadSchema(uri, callback) {
28
  request.json(uri, function(err, res, body) {
29
    if (err || res.statusCode >= 400)
30
      callback(err || new Error('Loading error: ' + res.statusCode));
31
    else
32
      callback(null, body);
33
  });
34
}

El código define la función validateNavigation que carga el esquema y compila la función de validación cuando se llama la primera vez y siempre devuelve el resultado de validación a través de la devolución de llamada. Hay varias maneras de mejorarlo, desde la precarga y la compilación del esquema por separado, antes de que se utilice la primera vez, para dar cuenta del hecho de que la función se puede llamar varias veces antes de haber administrado el caché del esquema (ajv.compileAsync ya asegura que el esquema siempre se solicita sólo una vez).

Ahora examinaremos las nuevas palabras clave que se proponen para la versión 5 del estándar de esquema JSON.

JSON-Schema Versión 5 Propuestas

Aunque estas propuestas no han sido finalizadas como un borrador estándar, pueden ser usadas hoy en día - el validador Ajv las implementa. Expandir sustancialmente lo que puede validar con JSON-schema, por lo que vale la pena usarlos.

Para utilizar todas estas palabras clave con Ajv, debe utilizar la opción v5: true.

Palabras clave "constant" y "contains"

Estas palabras clave se agregan para mayor comodidad.

La palabra clave "constant" requiere que los datos sean iguales al valor de la palabra clave. Sin esta palabra clave, podría haber sido logrado con la palabra clave "enum" con un elemento en la matriz de elementos.

Este esquema requiere que los datos sean iguales a 1:

1
{ "constant": 1 }

La palabra clave "contains" requiere que algún elemento de la matriz coincida con el esquema de esta palabra clave. Esta palabra clave sólo se aplica a las matrices; cualquier otro tipo de datos será válido según él. Es un poco más difícil expresar este requisito usando sólo palabras clave de la versión 4, pero es posible.

Este esquema requiere que si los datos son una matriz, al menos uno de sus elementos es entero:

1
{ "contains": { "type": "integer" } }

Es equivalente a éste:

1
{
2
  "not": {
3
    "type": "array",
4
    "items": {
5
      "not": { "type": "integer" }
6
    }
7
  }
8
}

Para que este esquema sea válido, los datos no deben ser una matriz o no deben tener todos sus elementos no enteros (es decir, algunos elementos deben ser enteros).

Tenga en cuenta que tanto la palabra clave "contains" como el esquema equivalente anterior fallarían si los datos fueran una matriz vacía.

Palabra clave "patternGroups"

Esta palabra clave se propone como un reemplazo de "patternProperties". Permite limitar el número de propiedades que coinciden con el patrón que debe existir en el objeto. Ajv soporta tanto "patternGroups" como "patternProperties" en el modo v5 porque el primero es mucho más detallado y si no quiere limitar el número de propiedades que prefiere usar con el segundo.

Por ejemplo, el esquema:

1
{
2
  "patternGroups": {
3
    "^[a-z]+$": {
4
      "schema": { "type": "string" }
5
    },
6
    "^[0-9]+$": {
7
      "schema": { "type": "number" }
8
    }
9
  }
10
}

es equivalente a este esquema:

1
{
2
  "patternProperties": {
3
    "^[a-z]+$": { "type": "string" },
4
    "^[0-9]+$": { "type": "number" }
5
  }
6
}

Ambos requieren que el objeto tenga solamente propiedades con las llaves que consisten solamente en letras minúsculas con los valores de la cadena del tipo y con las llaves que consisten solamente de números con los valores del número tipo. No requieren ninguna cantidad de tales propiedades, ni limitan el número máximo. Eso es lo que puedes hacer con "patternGroups":

1
{
2
  "patternGroups": {
3
    "^[a-z]+$": {
4
      "minimum": 1,
5
      "maximum": 3,
6
      "schema": { "type": "string" }
7
    },
8
    "^[0-9]+$": {
9
      "minimum": 1,
10
      "schema": { "type": "number" }
11
    }
12
  }
13
}

El esquema anterior tiene requisitos adicionales: debe haber al menos una propiedad que coincida con cada patrón y no más de tres propiedades cuyas teclas contengan solo letras.

No se puede lograr lo mismo con "patternProperties".

Palabras clave para limitar los valores formateados "formatMaximum" / "formatMaximum"

Estas palabras clave junto con "exclusiveFormatMaximum" / "exclusiveFormatMinimum" le permiten establecer límites de tiempo, fecha y potencialmente otros valores de cadena que tengan formato requerido con la palabra clave "format".

Este esquema requiere que los datos sean una fecha y sea mayor o igual que el 1 de enero de 2016:

1
{
2
  "format": "date",
3
  "formatMinimum": "2016-01-01"
4
}

Ajv admite la comparación de los datos formateados para los formatos "fecha", "hora" y "fecha-hora", y puede definir formatos personalizados que soporten los límites con las palabras clave "formatMaximum" / "formatMaximum".

Palabra clave "switch"

Si bien todas las palabras clave anteriores fueron o bien le permite expresar mejor lo que era posible sin ellos o ampliar ligeramente las posibilidades, no cambiar la naturaleza declarativa y estática del esquema. Esta palabra clave le permite hacer la validación dinámica y dependiente de los datos. Contiene varios casos if-then.

Es más fácil de explicar con un ejemplo:

1
{
2
  "switch": [
3
    { "if": { "minimum": 50 }, "then": { "multipleOf": 5 } },
4
    { "if": { "minimum": 10 }, "then": { "multipleOf": 2 } },
5
    { "if": { "maximum": 4 }, "then": false }
6
  ]
7
}

El esquema anterior valida secuencialmente los datos contra los subesquemas en palabras clave "if" hasta que uno de ellos pase la validación. Cuando esto sucede, valida el esquema en la palabra clave "then" en el mismo objeto, que será el resultado de la validación de todo el esquema. Si el valor de "then" es false, la validación fallará inmediatamente.

De esta manera, el esquema anterior requiere que el valor sea:

  • mayor o igual a 50 y es un múltiplo de 5
  • o entre 10 y 49 y un múltiplo de 2
  • o entre 5 y 9

Este conjunto particular de requisitos se puede expresar sin una palabra clave switch, pero hay casos más complejos cuando no es posible.

Tarea 4

Cree el esquema equivalente al último ejemplo anterior sin utilizar una palabra clave switch.

Ponga su respuesta en part2/task4/no_switch_schema.json y ejecute el node part2/task4/validate para comprobarlo.

Los casos de palabra clave "switch" también pueden contener la palabra clave "continue" con un valor booleano. Si este valor es true, la validación continuará después de que una coincidencia de esquema satisfactoria "si" coincida con la validación de esquema "then" satisfactoria. Esto es similar a un fall-through al siguiente caso en una sentencia switch de JavaScript, aunque en JavaScript fall-through es un comportamiento predeterminado y la palabra clave "switch" requiere una instrucción explícita de "continue". Este es otro ejemplo sencillo con una instrucción "continue":

1
"schema": {
2
  "switch": [
3
    { "if": { "minimum": 10 }, "then": { "multipleOf": 2 }, "continue": true },
4
    { "if": { "minimum": 20 }, "then": { "multipleOf": 5 } }
5
  ]
6
}

Si se satisface la primera condición "if" y se cumple el requisito "then", la validación continuará comprobando la segunda condición.

Referencia "$data"

La palabra clave "$data" amplía aún más lo que es posible con JSON-schema y hace que la validación sea más dinámica y dependa de los datos. Le permite poner valores de algunas propiedades de datos, elementos o claves en determinadas palabras clave de esquema.

Por ejemplo, este esquema define un objeto con dos propiedades en las que si ambos están definidos, "mayor" debe ser mayor o igual que "menor"; el valor en "menor" se utiliza como mínimo para "mayor":

1
"schema": {
2
  "properties": {
3
    "smaller": {},
4
    "larger": {
5
      "minimum": { "$data": "1/smaller" }
6
    }
7
  }
8
}

Ajv implementa la referencia "$data" para la mayoría de las palabras clave cuyos valores no son esquemas. Se falla la validación si la referencia "$data" hace referencia a un tipo incorrecto y tiene éxito si apunta al valor indefinido (o si la ruta de acceso no existe en el objeto).

¿Cuál es el valor de la cadena en la referencia "$data"? Parece similar a JSON-pointer pero no es exactamente eso. Es un puntero JSON relativo definido por este borrador estándar.

Consiste en un número entero que define cuántas veces la búsqueda debe recorrer el objeto (1 en el ejemplo anterior significa un padre directo) seguido de un puntero "#" o JSON.

Si el número es seguido de "#", entonces el valor que JSON-pointer resuelve será el nombre de la propiedad o el índice del elemento que tiene el objeto. De esta manera, "0 #" en lugar de "1 / menor" resolvería a la cadena "mayor", y "1 #" sería inválido ya que los datos enteros no son un miembro de ningún objeto o matriz. Este esquema:

1
{
2
  "type": "object",
3
  "patternProperties": {
4
    "^date$|^time$": { "format": { "$data": "0#" } }
5
  }
6
}

es equivalente a éste:

1
{
2
  "type": "object",
3
  "properties": {
4
    "date": { "format": "date" },
5
    "time": { "format": "time" }
6
  }
7
}

porque {"$ data": "0#"} se sustituye por el nombre de la propiedad.

Si el número en el puntero es seguido por JSON-puntero, entonces este puntero JSON se resuelve a partir del objeto primario al que se refiere este número. Puede ver cómo funciona en el primer ejemplo "más pequeño" / "más grande".

Veamos nuevamente nuestro ejemplo de navegación. Uno de los requisitos que puede ver en los datos es que la propiedad page_id del objeto de página es siempre igual a la propiedad parent_id del objeto de navegación contenido. Podemos expresar este requisito en el esquema page.json usando la referencia "$data":

1
{
2
  "$schema": "http://json-schema.org/draft-04/schema#",
3
  "id": "http://mynet.com/schemas/page.json#",
4
  ...
5
  "switch": [{
6
    "if": { "required": [ "navigation" ] },
7
    "then": {
8
      "properties": {
9
        "page_id": { "constant": { "$data": "1/navigation/parent_id" } }
10
      }
11
    }
12
  }]
13
}

La palabra clave "switch" añadida al esquema de página requiere que si el objeto de página tiene la propiedad de navegación navigation , el valor de la propiedad page_id debe ser el mismo que el valor de la propiedad parent_id en el objeto de navegación. Lo mismo se puede lograr sin la palabra clave "switch", pero es menos expresivo y contiene duplicación:

1
{
2
  ...
3
  "anyOf": [
4
    { "not": { "required": [ "navigation" ] } },
5
    {
6
      "required": [ "navigation" ],
7
      "properties": {
8
        "page_id": { "constant": { "$data": "1/navigation/parent_id" } }
9
      }
10
    }
11
  ]
12
}

Tarea 5

Ejemplos de JSON-punteros relativos pueden ser útiles.

Utilizando las palabras clave v5, defina el esquema para el objeto con dos propiedades obligatorias list y order. List debe ser una matriz que tiene hasta cinco números. Todos los artículos deben ser números y deben ser ordenados en orden ascendente o descendente, según lo determine la propiedad order que puede ser "asc" o "desc".

Por ejemplo, este es un objeto válido:

1
{
2
  "list": [ 1, 3, 3, 6, 9 ],
3
  "order": "asc"
4
}

y esto es inválido:

1
{
2
  "list": [ 9, 7, 3, 6, 2 ],
3
  "order": "desc"
4
}

Ponga su respuesta en part2/task5/schema.json y ejecute el node part2/task5/validate para comprobarlo.

¿Cómo crear un esquema con las mismas condiciones, pero para una lista de tamaño ilimitado?

Definición de nuevas palabras clave de validación

Hemos examinado las nuevas palabras clave que se proponen para la versión 5 del estándar de esquema JSON. Puede usarlos hoy, pero a veces puede querer más. Si ha realizado la tarea 5, probablemente haya notado que algunos requisitos son difíciles de expresar con JSON-schema.

Algunos validadores, incluyendo Ajv, le permiten definir palabras clave personalizadas. Palabras clave personalizadas

  • permiten crear escenarios de validación que no se pueden expresar mediante JSON-Schema
  • simplificar sus esquemas
  • le ayudará a llevar una mayor parte de la lógica de validación a sus esquemas
  • hacer que sus esquemas sean más expresivos, menos detallados y más cercanos a su dominio de aplicación

Uno de los desarrolladores que utiliza Ajv escribió en GitHub:

"Ajv con palabras clave personalizadas nos ha ayudado mucho con la validación de la lógica de negocios en nuestro backend. Consolidamos un montón de validaciones de nivel de controlador en JSON-Schema con palabras clave personalizadas. El efecto neto es mucho mejor que escribir código de validación individual ".

Las preocupaciones que debe tener en cuenta al ampliar el estándar de esquema JSON con palabras clave personalizadas son la portabilidad y la comprensión de sus esquemas. Tendrá que soportar estas palabras clave personalizadas en otras plataformas y documentar adecuadamente estas palabras clave para que todos puedan entenderlas en sus esquemas.

El mejor enfoque aquí es definir un nuevo meta-esquema que será la extensión del borrador 4 meta-esquema o "v5 propuestas" meta-esquema que incluirá tanto la validación de sus palabras clave adicionales y su descripción. A continuación, los esquemas que utilizan estas palabras clave personalizadas tendrán que establecer la propiedad $schema en el URI del nuevo meta-esquema.

Ahora que has sido advertido, nos sumergiremos y definiremos un par de palabras clave personalizadas con Ajv.

Ajv proporciona cuatro maneras de definir palabras clave personalizadas que puede ver en la documentación. Vamos a ver dos de ellos:

  • utilizando una función que compila su esquema a una función de validación
  • utilizando una macro-función que toma su esquema y devuelve otro esquema (con o sin palabras clave personalizadas)

Comencemos con el ejemplo simple de una palabra clave de rango. Un rango es simplemente una combinación de palabras clave mínima y máxima, pero si tienes que definir muchos rangos en tu esquema, especialmente si tienen límites exclusivos, puede resultar fácilmente aburrido.

Así es como debería verse el esquema:

1
{
2
  "range": [5, 10],
3
  "exclusiveRange": true
4
}

donde la gama exclusiva es opcional, por supuesto. El código para definir esta palabra clave es el siguiente:

1
ajv.addKeyword('range', { type: 'number', compile: compileRange });
2
ajv.addKeyword('exclusiveRange'); // this is needed to reserve the keyword

3
4
function compileRange(schema, parentSchema) {
5
  var min = schema[0];
6
  var max = schema[1];
7
8
  return parentSchema.exclusiveRange === true
9
          ? function (data) { return data > min && data < max; }
10
          : function (data) { return data >= min && data <= max; }
11
}

¡Y eso es! Después de este código, puede usar la palabra clave range en sus esquemas:

1
var schema = {
2
  "range": [5, 10],
3
  "exclusiveRange": true
4
};
5
6
var validate = ajv.compile(schema);
7
8
console.log(validate(5)); // false

9
console.log(validate(5.1)); // true

10
console.log(validate(9.9)); // true

11
console.log(validate(10)); // false

El objeto pasado a addKeyword es una definición de palabra clave. Opcionalmente, contiene el tipo (o los tipos como matriz) a los que se aplica la palabra clave. La función de compilación se llama con los parámetros schema y parentSchema y debe devolver otra función que valida los datos. Esto lo hace casi tan eficiente como las palabras clave nativas, porque el esquema se analiza durante su compilación, pero existe el coste de una llamada de función adicional durante la validación.

Ajv le permite evitar esta sobrecarga con palabras clave que devuelven el código (como una cadena) que se hará parte de la función de validación, pero es bastante complejo por lo que no lo veremos aquí. La forma más sencilla es utilizar palabras clave de macros: tendrá que definir una función que tome el esquema y devuelva otro esquema.

A continuación se muestra la implementación de la palabra clave range con una función de macro:

1
ajv.addKeyword('range', { type: 'number', macro: macroRange });
2
3
function macroRange(schema, parentSchema) {
4
  var resultSchema = {
5
    "minimum": schema[0],
6
    "maximum": schema[1]
7
  };
8
9
  if (parentSchema.exclusiveRange === true) {
10
    resultSchema.exclusiveMimimum = resultSchema.exclusiveMaximum = true;
11
  }
12
13
  return resultSchema;
14
}

Puede ver que la función simplemente devuelve el nuevo esquema que es equivalente a la palabra clave de rango range que utiliza las palabras clave máximo maximum y mínimo minimum.

Veamos también cómo podemos crear un meta-esquema que incluya la palabra clave range. Usaremos el meta-esquema del borrador 4 como nuestro punto de partida:

1
{
2
  "id": "http://mynet.com/schemas/meta-schema-with.range.json#",
3
  "$schema": "http://json-schema.org/draft-04/schema#",
4
  "allOf": [
5
    { "$ref": "http://json-schema.org/draft-04/schema#" },
6
    {
7
      "properties": {
8
        "range": {
9
          "description": "1st item is minimum, 2nd is maximum",
10
          "type": "array",
11
          "items": [ { "type": "number" }, { "type": "number" } ],
12
          "additionalItems": false 
13
        },
14
        "exclusiveRange": {
15
          "type": "boolean",
16
          "default": false
17
        }
18
      },
19
      "dependencies": {
20
        "exclusiveRange": [ "range" ]
21
      } 
22
    }
23
  ]
24
}

Si desea utilizar las referencias "$data" con la palabra clave range, tendrá que extender el esquema meta de "v5 proposals" que se incluye en Ajv (vea el enlace anterior) para que estas referencias puedan ser los valores de range y exclusiveRange . Y aunque nuestra primera implementación no soportará referencias de "$data", la segunda con una macro-función las soportará.

Ahora que tienes un meta-esquema, debes añadirlo a Ajv y usarlo en esquemas usando la palabra clave range:

1
ajv.addMetaSchema(require('./meta-schema-with-range.json'));
2
3
var schema = {
4
  "$schema": "http://mynet.com/schemas/meta-schema-with-range.json#",
5
  "range": [5, 10],
6
  "exclusiveRange": true
7
};
8
9
var validate = ajv.compile(schema);

El código anterior habría lanzado una excepción si los valores no válidos se pasaron a rango range o exclusivaRange.

Tarea 6

Suponga que ha definido la palabra clave jsonPointers que aplica los esquemas a las propiedades profundas definidas por los punteros JSON que apuntan a los datos que comienzan desde el actual. Esta palabra clave es útil con la palabra clave switch, ya que le permite definir requisitos para propiedades y elementos profundos. Por ejemplo, este esquema utilizando la palabra clave jsonPointers:

1
{
2
  "jsonPointers": {
3
    "0/books/2/title": { "pattern": "json|Json|JSON" },
4
  }
5
}

es equivalente a:

1
  {
2
    "properties": {
3
      "books": {
4
        "items": [
5
          {},
6
          {},
7
          {
8
            "properties": {
9
              "title": { "pattern": "json|Json|JSON" }
10
            }
11
          }
12
        ]
13
      }
14
    }
15
  }

Suponga que también ha definido la palabra clave requiredJsonPointers que funciona de forma similar a requerida required pero con punteros JSON en lugar de propiedades.

Si lo desea, también puede definir estas palabras clave, o puede ver cómo se definen en el archivo part2/task6/json_pointers.js.

Su tarea es: usar palabras clave jsonPointers y requiredJsonPointers, definir la palabra clave select que es similar a la instrucción switch de JavaScript y tiene la sintaxis a continuación (otherwisefallthrough son opcionales):

1
{
2
  "select": {
3
    "selector": "<relative JSON-pointer that starts from '0/'>",
4
    "cases": [
5
      { "case": <value1>, "schema": { <schema1> }, "fallthrough": true },
6
      { "case": <value2>, "schema": { <schema2> } },
7
      ...
8
    ],
9
    "otherwise": { <defaultSchema> }
10
  }
11
}

Esta sintaxis permite valores de cualquier tipo. Tenga en cuenta que fallthrough es diferente de continue en la palabra clave switch. fallthrough aplica el esquema del siguiente caso a los datos sin comprobar que el selector es igual al valor del siguiente caso (ya que es muy probable que no sea igual).

Coloque sus respuestas en part2/task6/select_keyword.js y part2/task6/v5-meta-with-select.json y ejecute node part2/task6/validate para comprobarlos.

Bonus 1: Mejore su implementación para también apoyar esta sintaxis:

1
{
2
  "select": {
3
    "selector": "<relative JSON-pointer that starts from '0/'>",
4
    "cases": {
5
      "<value1>": { <schema1> },
6
      "<value2>": { <schema2> },
7
      ...
8
    },
9
    "otherwise": { <defaultSchema> }
10
  }
11
}

Se puede utilizar si todos los valores son diferentes cadenas y no hay fallthrough.

Bonus 2: ampliar el meta-esquema "v5 proposals" para incluir esta palabra clave.

Otros usos de JSON-Schemas

Además de validar los datos, los esquemas JSON pueden usarse para:

  • generar la interfaz de usuario
  • generar datos
  • modificar datos

Puede consultar las bibliotecas que generan UI y datos si está interesado. No lo exploraremos, ya que está fuera del alcance de este tutorial.

Veremos el uso de JSON-schema para modificar los datos mientras se está validando.

Filtrado de datos

Una de las tareas comunes al validar los datos es quitar las propiedades adicionales de los datos. Esto le permite sanear los datos antes de pasarlos a la lógica de procesamiento sin fallar la validación del esquema:

1
var ajv = Ajv({ removeAdditional: true });
2
3
var schema = {
4
  "type": "object",
5
  "properties": {
6
    "foo": { "type": "string" }
7
  },
8
  "additionalProperties": false
9
};
10
11
var validate = ajv.compile(schema);
12
13
var data: { foo: 1, bar: 2 };
14
15
console.log(validate(data)); // true

16
console.log(data); // { foo: 1 };

Sin la opción removeAdditional, la validación habría fallado, ya que hay una propiedad adicional bar que no está permitida por el esquema.  Con esta opción, la validación pasa y la propiedad se elimina del objeto.

Cuando el valor de la opción removeAdditional es true, las propiedades adicionales se eliminan sólo si la palabra clave additionalProperties es falsa. Ajv también le permite eliminar todas las propiedades adicionales, independientemente de la palabra clave additionalProperties o propiedades adicionales que no se validan (si la palabra clave additionalProperties es el esquema). Consulte la documentación de Ajv para obtener más información.

Asignación de valores predeterminados a propiedades y elementos

El estándar de esquema JSON define la palabra clave "default" que contiene un valor que los datos deben tener si no está definido en los datos validados. Ajv le permite asignar estos valores predeterminados en el proceso de validación:

1
var ajv = Ajv({ useDefaults: true });
2
3
var schema = {
4
  "type": "object",
5
  "properties": {
6
    "foo": { "type": "number" },
7
    "bar": { "type": "string", "default": "baz" }
8
  },
9
  "required": [ "foo", "bar" ]
10
};
11
12
var data = { "foo": 1 };
13
14
var validate = ajv.compile(schema);
15
16
console.log(validate(data)); // true

17
console.log(data); // { "foo": 1, "bar": "baz" }

Sin la opción useDefaults, la validación habría fallado, ya que no hay ninguna propiedad requerida en el objeto validado. Con esta opción, la validación pasa y la propiedad con el valor predeterminado se agrega al objeto.

Coacción de tipos de datos

"type" es una de las palabras clave más usadas en JSON-schemas. Cuando está validando entradas de usuario, todas sus propiedades de datos que obtiene de los formularios suelen ser cadenas. Ajv le permite coaccionar los datos a los tipos especificados en el esquema, tanto para pasar la validación y para utilizar los datos correctamente introducidos después:

1
var ajv = Ajv({ coerceTypes: true });
2
var schema = {
3
  "type": "object",
4
  "properties": {
5
    "foo": { "type": "number" },
6
    "bar": { "type": "boolean" }
7
  },
8
  "required": [ "foo", "bar" ]
9
};
10
11
var data = { "foo": "1", "bar": "false" };
12
13
var validate = ajv.compile(schema);
14
15
console.log(validate(data)); // true

16
console.log(data); // { "foo": 1, "bar": false }

Comparación de JavaScript JSON-Schema Validadores

Hay más de diez validadores JavaScript activamente disponibles. ¿Cuál deberías usar?

Puede ver los valores de referencia del rendimiento y cómo los distintos validadores pasan el conjunto de pruebas del estándar JSON-schema en el proyecto json-schema-benchmark.

También hay rasgos distintivos que algunos validadores tienen que pueden hacerlos los más adecuados para su proyecto. Voy a comparar algunos de ellos a continuación.

es-mi-json-válido y jsen

Estos dos validadores son muy rápidos y tienen interfaces muy simples. Ambos compilar esquemas a las funciones de JavaScript, como hace Ajv.

Su desventaja es que ambos tienen apoyo limitado de referencias remotas.

schemasaurus

Esta es una biblioteca única en su tipo donde la validación de esquema JSON es casi un efecto secundario.

Está construido como un procesador / iterador de esquema JSON genérico y fácilmente extensible que puede usar para construir todo tipo de herramientas que usan JSON-schema: generadores de UI, plantillas, etc.

Ya tiene validador de esquema JSON relativamente rápido incluido.

Sin embargo, no admite referencias remotas en absoluto.

themis

El más lento en el grupo de los validadores rápidos, tiene un sistema comprensivo de características, con un apoyo limitado de referencias remotas.

Donde realmente brilla es su implementación de la palabra clave default. Si bien la mayoría de los validadores tienen un soporte limitado de esta palabra clave (Ajv no es una excepción), Themis tiene una lógica muy compleja de aplicar valores predeterminados con rollbacks dentro de palabras clave compuestas como anyOf.

z-schema

En términos de rendimiento, este validador muy maduro se encuentra en la frontera entre validadores rápidos y lentos. Probablemente fue uno de los más rápidos antes de que apareciera la nueva generación de validadores compilados (todo lo anterior y Ajv).

Pasa casi todas las pruebas del conjunto de pruebas JSON-schema para validadores, y tiene una implementación bastante completa de referencias remotas.

Tiene un gran conjunto de opciones que le permiten ajustar el comportamiento predeterminado de muchas palabras clave de esquema JSON (por ejemplo, no aceptar matrices vacías como matrices o conjuntos vacíos de cadenas) e imponer requisitos adicionales en los esquemas JSON (por ejemplo, requiera la palabra clave minLength para cadenas).

Creo que en la mayoría de los casos, tanto la modificación de comportamientos de esquema como la inclusión de peticiones a otros servicios en el esquema JSON son las cosas equivocadas que se deben hacer. Pero hay casos en que la capacidad de hacerlo simplifica mucho.

tv4

Este es uno de los validadores más antiguos (y más lentos) que soportan la versión 4 del estándar. Como tal, suele ser una opción predeterminada para muchos proyectos.

Si lo está utilizando, es muy importante entender cómo reporta errores y referencias que faltan y configurarlo correctamente, de lo contrario obtendrá muchos falsos positivos (es decir, la validación pasa con datos no válidos o referencias remotas no resueltas).

Los formatos no se incluyen de forma predeterminada, pero están disponibles como una biblioteca independiente.

Ajv

Escribí Ajv porque todos los validadores existentes eran rápidos o conformes con el estándar (especialmente con respecto a apoyar referencias remotas) pero no ambos. Ajv llenó esa brecha.

Actualmente es el único validador que:

  • pasa todas las pruebas y apoya completamente las referencias remotas
  • Soporta las palabras clave de validación propuestas para la versión 5 de las referencias estándar y $data   
  • soporta validación asíncrona de formatos y palabras clave personalizados

Tiene opciones para modificar el proceso de validación y para modificar datos validados (filtrado, asignación de valores predeterminados y tipos de coerción-vea los ejemplos anteriores).

¿Qué validador utilizar?

Creo que el mejor enfoque es probar varios y elegir el que funciona mejor para usted.

Escribí json-schema-consolidate, que suministra una colección de adaptadores que unifican las interfaces de 12 validadores de esquema JSON. Utilizando esta herramienta puede pasar menos tiempo cambiando entre validadores. Recomiendo quitarlo una vez que haya decidido qué validador usar, ya que mantenerlo afectaría negativamente al rendimiento.

¡Eso es todo! Espero que este tutorial sea útil. Usted ha aprendido sobre:

  • estructuración de sus esquemas
  •     usando referencias e IDs
  •    usando palabras clave de validación y $data reference de las propuestas de la versión 5
  • cargar esquemas remotos asincrónicamente
  •    definición de palabras clave personalizadas   
  • modificación de datos en el proceso de validación   
  • ventajas y desventajas de diferentes validadores de esquema JSON

¡Gracias por leer!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.