JSON Schema permite definir una estructura estricta para los datos transmitidos entre aplicaciones que se comunican.

Introducción

En las aplicaciones web es habitual trabajar con datos complejos que deben transmitirse entre distintas aplicaciones.

Tipo

Al deserializar un string JSON puedes definir su estructura mediante un tipo para proporcionar un mejor autocompletado, comprobación de errores y legibilidad.

Crea el fichero schema.ts

type Person = {
    name: string,
    age: number
}

let str = '{ "name": "Eva", "age": 33}'

let person: Person = JSON.parse(str)

console.log(person.name)

Si ejecutas este código, funciona sin problemas ya que el objeto deserializado tiene la propiedad name:

> bun.exe .\schema.ts
Eva

Sin embargo, esto es sólo informativo, ya que JSON.parse no verifica que el string contiene un objeto compatible con el tipo de la variable:

type Person = {
    name: string,
    age: number
}

let str = '{"brand": "Seat", "model": "Ibiza"}'

let person: Person = JSON.parse(str)

console.log(person.name)

Si ejecutas el código el resultado es undefined porque el objeto deserializado no tiene la propiedad name:

> bun.exe .\schema.ts
undefined

Y aunque en este caso el problema sea inocuo, no sucede lo mismo con este código:

type Person = {
    name: string,
    age: number
    address: Address 
}

type Address = {
    street: string,
    city: string
}

let str = '{"brand": "Seat", "model": "Ibiza"}'

let person: Person = JSON.parse(str)

console.log(person.address.city)

Si lo ejecutas, tienes un error grave en tiempo de ejecución:

> bun.exe .\schema.ts
12 |
13 | let str = '{"brand": "Seat", "model": "Ibiza"}'
14 |
15 | let person: Person = JSON.parse(str)
16 |
                        ^
TypeError: undefined is not an object (evaluating 'person.address.city')
      

Esquema

Un objeto JSON no incluye ningún contexto ni metadatos, y no hay forma de saber con solo mirar el objeto qué significan las propiedades o cuáles son los valores admitidos.

A continuación tienes un objeto que pertenece a un catalogo de productos:

{
    "id": 65,  
    "name": "A green door",  
    "price": 12.50,  
    "tags": [ "home", "green" ]
}

Un esquema es un documento JSON que contiene la descripción y restricciones que tiene que tener un documento JSON para que sea conforme a ese esquema y, por tanto, una instancia de ese esquema.

El esquema más básico es un objeto JSON en blanco, que no restringe nada, no permite nada y no describe nada:

{}

Agregando propiedades de validación al esquema puedes aplicar restricciones a una instancia.

Por ejemplo, puedes usar la propiedad "type" para restringir una instancia a un objeto, array, string, number, boolean o null:

{ "type": "string" }

Para crear una definición de esquema básica, define las siguientes propiedades:

  • $schema: especifica a qué "draft" del estándar de esquema JSON se adhiere el esquema.
  • $id: establece una URI para el esquema. Puedes utilizar esta URI única para hacer referencia a elementos del esquema desde dentro del mismo documento o desde documentos JSON externos.
  • title y description: indican la intención del esquema. Estas propiedades no añaden ninguna restricción a los datos que se están validando.
  • type: define la primera restricción de los datos JSON. En el ejemplo del catálogo de productos que aparece a continuación, esta propiedad especifica que los datos deben ser un objeto JSON.

Por ejemplo:

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://example.com/product.schema.json",
    "title": "Product",
    "description": "A product in the catalog",
    "type": "object"
}

Propiedades

properties es una propiedad de validación que consiste en un objeto donde cada propiedad representa una propiedad de los datos JSON que se están validando.

También puedes especificar qué propiedades definidas en el objeto son obligatorias.

Usando el ejemplo del catálogo de productos, id es un valor numérico que identifica de forma única un producto. Dado que este es el identificador canónico del producto, es obligatorio.

Agrega la propiedad de validación properties al esquema:

{
    ...
    "type": "object",
    "properties": {}
}

Agrega la propiedad id junto con las siguientes anotaciones:

  • description: indica que id es el identificador único del producto.
  • type define el tipo de datos que son válidos, en este caso el identificador del producto debe ser un número.
{
    ...
    "type": "object",
    "properties": {
        "id": {
            "description": "The unique identifier for a product",
            "type": "integer"
        }
    }
}

Agrega la propiedad name de tipo string:

{
    ...
    "type": "object",
    "properties": {
        "id": {
            "description": "The unique identifier for a product",
            "type": "integer"
        },
        "name": {
            "description": "Name of the product",
            "type": "string"
        }
    }
}

Continua llegint: https://json-schema.org/learn/getting-started-step-by-step

Ajv

Ajv es un validador al que le pasas un esquema para tus datos JSON y lo convierte en un código TypeScript muy eficiente que valida tus datos de acuerdo con el esquema.

Instala ajv:

> bun add ajv

Crea un esquema de validación con dos propiedades id y name:

const productSchema = {
    type: "object",
    properties: {
        id: { type: "integer" },
        name: { type: "string" }
    }
}

A continuación crea una función de validación a partir de este esquema:

import Ajv from "ajv"

const productSchema = {
    type: "object",
    properties: {
        id: { type: "integer" },
        name: { type: "string" }
    }
}

const ajv = new Ajv()
const validateProduct = ajv.compile(productSchema)

Ajv compila el esquema en un función y la almacena en caché (usando el esquema mismo como clave en un mapa), de modo que la próxima vez que se use el mismo objeto de esquema no se volverá a compilar.

Ya puedes utilizar la función que has creado para validar si un objecto es conforme al schema productSchema:

const apple = { "id": 1, "name": "apple" }

if (!validateProduct(apple)) {
    console.log("apple is not an apple 🤨")
} else {
    console.log("Let's eat the 🍎")
}

Si ejecutas el código puedes ver que nos podemos comer la manzana porque es un objecto que tiene las propiedades que se le pide a un producto:

> bun .\schema.ts
Let's eat the 🍎

Y no nos comemos la manzana si no tiene las propiedades requeridas:

const validateProduct = ajv.compile(productSchema)

const apple = { "name": "apple" }

if (!validateProduct(apple)) {
    console.log("apple is not a product 🤨")
} else {
    console.log("Let's eat the 🍎")
}

Pero ya sabes que TypeScript utiliza "duck typing".

Por tanto, este objecto que representa una persona, aunque sea correcto a nivel de código no está permitido en la vida real ...

const eva = { "id": 1, "name": "Eva" }

if (!validateProduct(eva)) {
    console.log("eva is not an product 🤨")
} else {
    console.log("Let's eat 👩")
}

Ya se, ya se, hace muchos años Eva podía ser un producto comestible, pero hoy en día no se admite 🫡 ... los estudiantes soy demasiado flexibles en el momento de revisar la nota.

De todas formas, este producto, aunque didáctico, no está muy bien diseñado para lo que lo estamos utilizando.

Modifica el esquema del producto y mejora el código:

import Ajv from "ajv"

const productSchema = {
    type: "object",
    properties: {
        id: { type: "integer" },
        name: { type: "string" },
        icon: { type: "string" },
        edible: { type: "boolean" }
    },
    required: ["id", "name", "icon", "edible"]
}

const ajv = new Ajv()
const validateProduct = ajv.compile(productSchema)

const product = JSON.parse('{ "id": 1, "name": "orange", "icon": "🍊", "edible": true }')

if (!validateProduct(product)) {
    console.log("product is not a product 🤨")
} else {
    if (product.edible) {
        console.log(`Let's eat the ${product.icon}`)
    }
}

Puedes ver que ahora nos comemos la naranja 🍊 porque és un producto comestible ("edible").

Y ... todavía se puede mejorar más porque TypeScript és "script" con tipos:

import Ajv from "ajv"

type Product = {
    id: number,
    name: string,
    icon: string,
    edible: boolean
}

const productSchema = {
    type: "object",
    properties: {
        id: { type: "integer" },
        name: { type: "string" },
        icon: { type: "string" },
        edible: { type: "boolean" }
    },
    required: ["id", "name", "icon", "edible"]
}

const ajv = new Ajv()
const validateProduct = ajv.compile(productSchema)

const product: Product = JSON.parse('{ "id": 1, "name": "orange", "icon": "🍊", "edible": true }')

if (!validateProduct(product)) {
    console.log("product is not a Product 🤨")
} else {
    if (product.edible) {
        console.log(`Let's eat the ${product.icon}`)
    }
}

Sigue: https://ajv.js.org/guide/typescript.html

Schema generator

Fetch

TODO