Validar documentos JSON con JSON Schema

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

Los documentos JSON son una forma de intercambiar información entre aplicaciones. Como en cualquier intercambio de información es conveniente validar los datos recibidos antes de realizar ninguna acción. En Java dependiendo de la librería o framework utilizada aunque los datos se transmiten en formato JSON estos son transformados y recibidos como objetos Java en los cuales se realizan validaciones de tipos y conversiones de tipos y restricciones a los valores con Bean Validation o Spring Validation. La especificación JSON Schema permite definir un esquema para los documentos JSON independiente del lenguaje con la que realizar validaciones y realizar las validaciones a través de implementaciones en los diferentes lenguajes de programación incluido Java.

Java

Una tarea fundamental en toda aplicación es validar los datos de entrada. Validar los datos evita errores al procesar los datos, generar datos erróneos como resultado o realizar acciones en base a datos no válidos con consecuencias como realizar acciones no deseadas o crear inconsistencias en la base de datos. También hay que validar los datos por motivos de seguridad.

Los datos de entrada de un programa se proporcionan en función de la naturaleza de la aplicación, en una aplicación web o REST es a través de los datos de la petición, en una aplicación que procesa mensajes de servicios como Kafka o RabbitMQ los datos se incluyen en los datos del mensaje y en una aplicación de procesos batch los datos quizá estén en archivos.

Los formatos más comunes para el intercambio de datos son XML, JSON y CSV. El formato de documentos XML permite comprobar está bien formado en cuanto a balanceo de etiquetas junto a otros requerimientos, los esquemas XML permiten validar además si un documento XML cumple con el esquema incluyendo las etiquetas requeridas. JSON es una especificación que de por si no define ningún esquema, esto hace que los documentos JSON puedan tener cualesquiera datos mientras utilicen una sintaxis correcta o tengan un formato correcto. Sin embargo, en la validación de datos el que un documento JSON tenga una sintaxis correcta no es suficiente que se considere válido. Un esquema permite definir que un documento incluya ciertas propiedades, que estás se ajusten a unos valores predeterminados, que cumplan ciertas reglas de validación como un tipo o rango de valores.

La especificación JSON Schema es el equivalente para los documentos JSON de XML Schema para los documentos XML. JSON Schema permite validar que un documento JSON se ajusta a un esquema conteniendo los datos y valores definidos en el esquema.

La especificación JSON Schema

La especificación de JSON Schema tiene varias definiciones formales y versiones. En la guía de inicio paso a paso se incluye una descripción más sencilla y práctica para un primer inicio.

Un esquema de JSON contiene a qué versión de la especificación se ajusta, el identificador o la ubicación del esquema, un título, una descripción y el tipo de objeto del documento raíz. Además define qué propiedades junto con sus tipos ha de contener el documento JSON al que se aplica, cuáles de esas propiedades son requeridas y las validaciones sobre los datos como restricciones en los valores de los datos o elementos de un array. Además de propiedades un documento permite la anidación de estructuras en las que también se definen que propiedades contienen y cuáles son requeridas. Finalmente, un esquema JSON permite referenciar un esquema JSON externo.

En el siguiente esquema $schema define la versión del esquema que implementa, $id define el identificador del esquema. Las propiedades title, description proporcionan una descripción del esquema y type el tipo de objeto raíz. En la propiedad properties se definen las propiedades del documento JSON y en la propiedad required cuáles de esas propiedades son requeridas. En la propiedad dimensions están las estructuras JSON anidadas. Y con la propiedad $ref se referencia otro esquema JSON.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://example.com/product.schema.json",
    "title": "Product",
    "description": "A product from Acme's catalog",
    "type": "object",
    "properties": {
      "productId": {
        "description": "The unique identifier for a product",
        "type": "integer"
      },
      "productName": {
        "description": "Name of the product",
        "type": "string"
      },
      "price": {
        "description": "The price of the product",
        "type": "number",
        "exclusiveMinimum": 0
      },
      "tags": {
        "description": "Tags for the product",
        "type": "array",
        "items": {
          "type": "string"
        },
        "minItems": 1,
        "uniqueItems": true
      },
      "dimensions": {
        "type": "object",
        "properties": {
          "length": {
            "type": "number"
          },
          "width": {
            "type": "number"
          },
          "height": {
            "type": "number"
          }
        },
        "required": [
          "length",
          "width",
          "height"
        ]
      },
      "warehouseLocation": {
        "description": "Coordinates of the warehouse where the product is located.",
        "$ref": "https://example.com/geographical-location.schema.json"
      }
    },
    "required": [
      "productId",
      "productName",
      "price"
    ]
  }
product.schema.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
    "$id": "https://example.com/geographical-location.schema.json",
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "title": "Longitude and Latitude",
    "description": "A geographical coordinate on a planet (most commonly Earth).",
    "required": [
      "latitude",
      "longitude"
    ],
    "type": "object",
    "properties": {
      "latitude": {
        "type": "number",
        "minimum": -90,
        "maximum": 90
      },
      "longitude": {
        "type": "number",
        "minimum": -180,
        "maximum": 180
      }
    }
  }
geographical-location.schema.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "productId": 1,
  "productName": "An ice sculpture",
  "price": 12.5,
  "tags": [
    "cold",
    "ice"
  ],
  "dimensions": {
    "length": 7,
    "width": 12,
    "height": 9.5
  },
  "warehouseLocation": {
    "latitude": -78.75,
    "longitude": 20.4
  }
}
product.json
1
2
{
}
product-invalid.json

Librerías JSON Schema en Java

Hay varias librerías Java que implementan validación de JSON con la especificación de JSON Schema, junto a otras implementaciones en otros lenguajes. De entre las implementaciones Java una de ellas es JSON Schema Validator de networknt en la que los errores que se detectan son devueltos en una estructura de datos en vez de lanzar una excepción en caso de que la validación falle.

Otra de sus funcionalidades es que permite hacer una correspondencia entre los identificadores de los esquemas JSON a recursos locales, útil en caso de que los esquemas no estén publicados en sus direcciones, en aplicaciones en las que no tengan conexión a internet o no se desea que estas realicen conexiones externas.

Hay que tener en cuenta que varias de estas librerías están implementadas por personas sin seguramente el respaldo de una organización, hay que tenerlo en cuenta como criterio de decisión en el caso de añadir como dependencia de un proyecto una de las implementaciones.

Ejemplo con Java de validar un JSON con JSON Schema

Este es un ejemplo que a partir de un documento JSON se valida que cumple el esquema contra el que se valida. En el caso de que el documento JSON no cumpla el esquema se devuelven los errores como resultado del método de validación, en el caso del ejemplo los errores son emitidos a la salida estándar donde se aprecia que en el caso de la validación del JSON inválido faltan las tres propiedades requeridas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package io.github.picodotdev.blogbitix.jsonschema;

import java.net.URI;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;

public class Main {

    public static void main(String[] args) throws Exception {
        Map<String, String> urlMappings = Map.of("https://picodotdev.github.io/blog-bitix/misc/json/product.schema.json", "resource:/product.schema.json",
                "https://picodotdev.github.io/blog-bitix/misc/json/geographical-location.schema.json", "resource:/geographical-location.schema.json");
        ObjectMapper mapper = new ObjectMapper();
        JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909))
                .objectMapper(mapper)
                .addUriMappings(urlMappings)
                .build();

        JsonSchema schema = factory.getSchema(URI.create("resource:/product.schema.json"));

        {
            JsonNode json = mapper.readTree(Main.class.getResourceAsStream("/product.json"));
            Set<ValidationMessage> errors = schema.validate(json);
            System.out.printf("Valid JSON errors: %d5n", errors.size());
        }

        {
            JsonNode json = mapper.readTree(Main.class.getResourceAsStream("/product-invalid.json"));
            Set<ValidationMessage> errors = schema.validate(json);
            System.out.printf("Valid JSON errors: %d%n", errors.size());
            errors.stream().forEach(it -> {
                System.out.printf("Type: %s%n", it.getType());
                System.out.printf("Message: %s%n", it.getMessage());
            });
        }
    }
}
Main.java
1
2
3
4
5
6
7
8
Valid JSON errors: 0
Invalid JSON errors: 3
Type: required
Message: $.productId: is missing but it is required
Type: required
Message: $.productName: is missing but it is required
Type: required
Message: $.price: is missing but it is required
System.out

La librería de JSON Schema Validator además de su propia dependencia requiere incluir otras en el archivo de construcción, en este caso de Gradle.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
plugins {
    id 'application'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.networknt:json-schema-validator:1.0.64'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
    implementation 'org.slf4j:slf4j-api:1.7.32'
    implementation 'org.apache.commons:commons-lang3:3.12.0'
}

application {
    mainClass = 'io.github.picodotdev.blogbitix.jsonschema.Main'
}
build.gradle

Otras formas de validación con Bean Validation y Spring Validation

Otra forma de validar un JSON es cargarlo en un objeto Java y validar el objeto con Bean Validation o Spring Validation. La diferencia en este caso respeto a JSON Schema es que Bean Validation y Spring Validation es una solución específica de Java, requiere cargar los datos en objetos y más importante no se define ningún esquema sino que el esquema está implícito en las validaciones ya se definan con anotaciones o con validadores personalizados.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: