JSON válido, datos erróneos
Validar JSON con un esquema es comparar cada campo de un registro con una lista de reglas. Las reglas dicen su tipo, si es obligatorio y qué valores se permiten. Importa porque un JSON que se analiza bien aún puede traer un número de páginas igual a «muchas».
La aplicación de una biblioteca pide a un modelo la ficha de un libro. La respuesta se analiza sin fallos. Luego un número de páginas llega como el texto «180» y la página de totales muestra disparates.
Al final de esta lección
Nota
Puedo comprobar una respuesta con un esquema.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Esquema | una lista de comprobación de los campos de un registro | |
| Tipo | la clase de valor que guarda un campo | integer |
| Obligatorio | un campo que debe estar presente | |
| Valores permitidos | los únicos valores que un campo puede tomar | novela, historia, ciencia |
| Fallo | una manera de romper una regla | |
| Registro | un objeto con un conjunto de campos |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un esquema es una lista de comprobación
Analizar dice que el texto es JSON. No dice que los datos sean correctos. Para eso hace falta un esquema: una regla por campo.
Una regla indica el tipo del campo, si es obligatorio y a veces sus valores permitidos. Un estándar llamado JSON Schema escribe esas reglas con un formato fijo. Aquí escribes tu propia lista de comprobación, en código.
Ejemplo resuelto: un esquema para un libro
| Campo | Tipo | Obligatorio | Valores permitidos |
|---|---|---|---|
| title | string | sí | cualquier texto |
| pages | integer | sí | cualquier número entero |
| genre | string | sí | novela, historia, ciencia |
| rating | number | no | cualquier número |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Reúne todos los fallos, no solo el primero
Una comprobación que se detiene en el primer fallo dice poco. Reúne todos los fallos en una lista, un texto corto para cada uno, como pages: tipo incorrecto.
La lista sirve para dos cosas. Decide si se acepta el registro, y la lección siguiente la devuelve al modelo como aviso.
Escribir la ayuda del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"estado \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la ayuda del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' const list = [].concat(sample);' ' const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return list[Math.min(turn, list.length - 1)];' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la comprobación, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' '// Un esquema es una lista de comprobación: una regla por campo.' 'const schema = [' ' { name: \"title\", type: \"string\", required: true },' ' { name: \"pages\", type: \"integer\", required: true },' ' { name: \"genre\", type: \"string\", required: true, allowed: [\"novela\", \"historia\", \"ciencia\"] },' ' { name: \"rating\", type: \"number\", required: false },' '];' > check2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la comprobación, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function isRecord(x) {' ' if (x === null) return false;' ' if (Array.isArray(x)) return false;' ' return typeof x === \"object\";' '}' '' 'function typeOk(value, type) {' ' if (type === \"string\") return typeof value === \"string\";' ' if (type === \"integer\") return Number.isInteger(value);' ' if (type === \"number\") return typeof value === \"number\";' ' if (type === \"boolean\") return typeof value === \"boolean\";' ' return false;' '}' >> check2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la comprobación, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Reunir todos los fallos, no solo el primero.' 'function faults(record, schema) {' ' if (isRecord(record) === false) return [\"no es un objeto\"];' ' const found = [];' ' for (const rule of schema) {' ' if (Object.prototype.hasOwnProperty.call(record, rule.name) === false) {' ' if (rule.required) found.push(rule.name + \": falta\");' ' continue;' ' }' ' const value = record[rule.name];' ' if (typeOk(value, rule.type) === false) found.push(rule.name + \": tipo incorrecto\");' ' else if (rule.allowed && rule.allowed.includes(value) === false) found.push(rule.name + \": valor no permitido\");' ' }' ' return found;' '}' >> check2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la comprobación, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function pullJson(text) {' ' const start = text.indexOf(\"{\");' ' const end = text.lastIndexOf(\"}\");' ' if (start === -1 || end < start) return null;' ' try { return JSON.parse(text.slice(start, end + 1)); } catch (error) { return null; }' '}' '' 'const records = [' ' { title: \"Atlas de ríos\", pages: 240, genre: \"ciencia\" },' ' { title: \"Tablas de mareas\", pages: \"180\", genre: \"historia\" },' ' { title: \"Cartas de luna\", pages: 96, genre: \"poesía\", rating: 4.5 },' ' { pages: 310, genre: \"novela\", rating: \"bueno\" },' '];' 'records.forEach(function (record, at) {' ' const found = faults(record, schema);' ' console.log(\"Registro \" + (at + 1) + \":\", found.length === 0 ? \"ok\" : found.join(\", \"));' '});' >> check2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la comprobación, parte 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const messages = [{ role: \"user\", content: \"Inventa un libro para una biblioteca universitaria. Responde con un objeto JSON con las claves title (texto), pages (un número entero) y genre (novela, historia o ciencia). Responde solo con el JSON.\" }];' ' // Muestra grabada, usada solo si ningún modelo responde. Rompe dos reglas a propósito.' ' const sample = JSON.stringify({ title: \"El puerto tranquilo\", pages: \"210\", genre: \"policíaca\" });' ' const reply = await ask(messages, sample, 100);' ' const record = pullJson(reply);' ' const found = faults(record, schema);' ' console.log(\"Registro del modelo:\", JSON.stringify(record));' ' console.log(\"Fallos:\", found.length === 0 ? \"ninguno\" : found.join(\", \"));' '}' 'main();' >> check2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejecutar la comprobación
docker exec -w /home/node/project agent node check2.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Una salida posible
| Línea | Lo que muestra |
|---|---|
| Registro 1 | ok |
| Registro 2 | pages: tipo incorrecto |
| Registro 3 | genre: valor no permitido |
| Registro 4 | title: falta, rating: tipo incorrecto |
| Registro del modelo y Fallos | Cambian en cada ejecución: un registro del modelo y sus fallos, o ninguno |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un error frecuente
Advertencia
Mucha gente cree que «180» y 180 son lo mismo. Se parecen, pero un programa no puede sumar ni comparar un texto como si fuera un número. Comprueba el tipo, no el aspecto del valor.