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.
Crea un proyecto nuevo con el nombre json-schema
:
> mkdir json-schema
> cd json-schema
> bun init
Tipo
Al deserializar un string
JSON puedes definir su estructura mediante un tipo para proporcionar un mejor autocompletado, comprobación de errores y legibilidad.
Modifica el fichero index.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 .\index.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 .\index.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 .\index.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.
Crea un fichero product.json
.
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
ydescription
: 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 queid
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" }
},
required: ["id", "name"]
}
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 .\index.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 son 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
A veces te interesa generar un esquema JSON a partir de la definición de tipos TypeScript.
Crea el fichero post.ts
:
export type Post = {
id: string
createdAt: string
updatedAt: string
title: string
content: string | null
published: boolean
authorId: string
}
A continuación, genera un esquema JSON para la interfaz Post
:
> bunx ts-json-schema-generator -p post.ts --type 'Post' -f .\tsconfig.json
{
"$ref": "#/definitions/Post",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Post": {
...
Puedes ver que el esquema se imprime en el terminal.
Si quieres puedes guardar el esquema en el fichero post.json
redirigiendo la salida:
> bunx ts-json-schema-generator -p post.ts --type 'Post' -f .\tsconfig.json > post.json
Otra opción es generar el esquema mediante un script.
Instala la librería ts-json-schema-generator
:
> bun add -d ts-json-schema-generator
Modifica el fichero schema.ts
:
export { }
import tsj from "ts-json-schema-generator"
/** @type {import('ts-json-schema-generator/dist/src/Config').Config} */
const config = {
path: "./post.ts",
tsconfig: "./tsconfig.json",
type: "*", // O <type-name> si sólo quieres generar un esquema para un tipo concreto
}
const outputPath = "post.json";
const schema = tsj.createGenerator(config).createSchema(config.type);
const schemaString = JSON.stringify(schema, null, 2);
await Bun.write(outputPath, schemaString)
Ya puedes generar el esquema vía código (e integrar este proceso en la construcción del proyecto)
bun .\schema.ts
Puedes importar el fichero JSON al script post.ts
:
import postSchema from './post.json'
Y utilizarlo para validar objectos tipo Post
:
import Ajv from 'ajv'
import postSchema from './post.json' with { type: "json" }
export type Post = {
id: string
createdAt: string
updatedAt: string
title: string
content: string | null
published: boolean
authorId: string
}
const ajv = new Ajv()
const validatePost = ajv.compile(postSchema)
const post: Post = JSON.parse('{ "id": 12345, "title": "JSON validation" }')
console.log(`post validated? ${validatePost(post)}`)
Fetch
Normalmente la validación se ejecuta con datos externo que consigues mediante APIs REST.
Es muy importante que valides esos datos porque nunca puedes estar seguro que los datos que recibes sean correctos.
A continuación utilizaremos datos "fake" de https://jsonplaceholder.typicode.com/.
Modifica el fichero index.ts
:
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const users = await response.json()
console.log(users.map((user: { name: string }) => user.name))
Este código baja un JSON con todos los usuarios y muestra por terminal el nombre de los usuarios:
bun run .\index.ts
[ "Leanne Graham", "Ervin Howell", "Clementine Bauch", "Patricia Lebsack", "Chelsey Dietrich",
"Mrs. Dennis Schulist", "Kurtis Weissnat", "Nicholas Runolfsdottir V", "Glenna Reichert",
"Clementina DuBuque"
]
A continuación crea el fichero typicode.ts
con los tipos correspondientes:
export type User = {
id: string
name: string
username: string
email: string
address: Address
phone: string
website: string
company: Company
}
type Address = {
street: string
suite: string
city: string
zipcode: string
geo: Geo
}
type Geo = {
lat: number
lng: number
}
type Company = {
name: string
catchPhrase: string
bs: string
}
Utiliza el tipo User
del script typicode.ts
y muestra el nombre de las empresas dónde trabajan los usuarios:
import type { User } from "./typicode"
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const users: User[] = await response.json()
console.log(users.map(user => user.company.name))
Crea el fichero typicode.json
con el esquema de validación:
> bunx ts-json-schema-generator -p .\typicode.ts -f .\tsconfig.json > .\typicode.json
Valida los datos de usuarios:
import Ajv from "ajv"
import type { User } from "./typicode"
import userSchema from "./tipycode.json"
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const users: User[] = await response.json()
const ajv = new Ajv()
const validateUser = ajv.compile(userSchema)
users.filter(user => validateUser(user))
console.log(users.map(user => user.company.name))
Actividad - DummyJSON
En DummyJSON tienes datos de prueba bastante reales
1.- En la url https://dummyjson.com/recipes tienes una lista de recetas.
import type { Recipe } from "./dummyjson"
const response = await fetch('https://dummyjson.com/recipes')
const data = await response.json()
const recipes: Recipe[] = data.recipes
console.log(recipes.map(recipe => recipe.name))
Crea el fichero dummyjson.ts
con los tipos correspondientes, el esquema de validación, etc.
dummyjson.ts
export type Recipe = {
id: number
name: string
ingredients: string[]
instructions: string[]
prepTimeMinutes: number
cookTimeMinutes: number
servings: number
difficulty: string
cuisine: string
caloriesPerServing: number
tags: string[]
userId: number
image: string
rating: number
reviewCount: number
mealType: string[]
}
2.- Haz lo mismo con otro tipo de datos
Acitividad - AEMET
Ves a la página de AEMET y registra-te para obtener una API Key: AEMET OpenData.
Crea el fichero aemet.ts
Guarda tu API Key en una variable (esta es del profesor)
const API_KEY = 'eyJzdWIiOiJkZGVtaW5nb0B4dGVjLmNhdCIsImp0aSI6ImE4N2I3M2Y5LWQ0YWYtNDc2My1hMzcwLTZlOTZjZjRiM2UxYSIsImlzcyI6IkFFTUVUIiwiaWF0IjoxNzI0MTc4MTg4LCJ1c2VySWQiOiJhODdiNzNmOS1kNGFmLTQ3NjMtYTM3MC02ZTk2Y2Y0YjNlMWEiLC'
En esta página tienes una descripción de todos los servicios de consulta disponibles:AEMET OpenData - Data specification
Si miras la documentació puedes ver que hay un endpoint que te permite consultar la predicción específica diaria de un municipio: /api/prediccion/especifica/municipio/diaria/{municipio}
.
Los códigos de población los puedes encontrar en este enlace: Código de municipio.
A continuación consultaremos la predicción específica diaria del municipio de barcelona que tiene el código 08019:
export {}
const API_KEY = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkZGVtaW5nb0B4dGVjLmNhdCIsImp0aSI6ImE4N2I3M2Y5LWQ0YWYtNDc2My1hMzcwLTZlOTZjZjRiM2UxYSIsImlzcyI6IkFFTUVUIiwiaWF0IjoxNzI0MTc4MTg4LCJ1c2VySWQiOiJhODdiNzNmOS1kNGFmLTQ3NjMtYTM3MC02ZTk2Y2Y0YjNlMWEiLCJyb2xlIjoiIn0.oWM1ZjREwwETKbc7ezINar03jJu832wR883EQYI6o_w'
const response = await fetch(`https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/diaria/08019/?api_key=${API_KEY}`)
const data = await response.json()
console.log(data)
El resultado es un fichero JSON donde estan los datos:
> bun run .\aemet.ts
{
descripcion: "exito",
estado: 200,
datos: "https://opendata.aemet.es/opendata/sh/a5a38371",
metadatos: "https://opendata.aemet.es/opendata/sh/dfd88b22",
}
puedes hacer un fecth a datos
:
export {}
const API_KEY = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkZGVtaW5nb0B4dGVjLmNhdCIsImp0aSI6ImE4N2I3M2Y5LWQ0YWYtNDc2My1hMzcwLTZlOTZjZjRiM2UxYSIsImlzcyI6IkFFTUVUIiwiaWF0IjoxNzI0MTc4MTg4LCJ1c2VySWQiOiJhODdiNzNmOS1kNGFmLTQ3NjMtYTM3MC02ZTk2Y2Y0YjNlMWEiLCJyb2xlIjoiIn0.oWM1ZjREwwETKbc7ezINar03jJu832wR883EQYI6o_w'
let response = await fetch(`https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/diaria/08019/?api_key=${API_KEY}`)
let data = await response.json()
response = await fetch(data.datos)
data = await response.json()
console.log(data)
Tendríamos que comprobar que el estado es 200
, pero al principi hay que escribir un código que funcione, más adelante ya lo modificarás para que funcione bien.
> bun run .\aemet.ts
[
{
origen: {
productor: "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a",
web: "https://www.aemet.es",
enlace: "https://www.aemet.es/es/eltiempo/prediccion/municipios/barcelona-id08019",
language: "es",
copyright: "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.",
notaLegal: "https://www.aemet.es/es/nota_legal",
},
elaborado: "2025-02-24T21:02:11",
nombre: "Barcelona",
provincia: "Barcelona",
prediccion: {
dia: [
[Object ...], [Object ...], [Object ...], [Object ...], [Object ...], [Object ...], [Object ...]
],
},
id: 8019,
version: 1,
}
]
A continuació modifica el código para que muestre las predicciones de temperatura máxima de cada día:
response = await fetch(data.datos)
data = await response.json()
console.log(data[0].prediccion.dia.map (dia => dia.temperatura.maxima))
A continuación:
- Crea los tipos y el esquema de validacion correspondiente
- Pregunta al usuario la ciudad que hay que consultar.