Valid JSON, wrong data
Validating JSON against a schema means checking each field of a record against a list of rules. The rules cover its type, whether it is required and which values are allowed. It matters because JSON that parses can still hold a page count of "many".
A library app asks a model for a book record. The reply parses without a fault. Then a page count arrives as the text "180" and the totals page prints nonsense.
By the end of this lesson
Note
I can check a JSON record against a schema and list every fault.
Key words
| Word | What it means | Example |
|---|---|---|
| Schema | a checklist of the fields a record must have | |
| Type | the kind of value a field holds | integer |
| Required | a field that must be present | |
| Allowed values | the only values a field may take | fiction, history, science |
| Fault | one way a record breaks a rule | |
| Record | one object with a set of fields |
The cells students fill in are left blank in this preview.
A schema is a checklist
Parsing says the text is JSON. It does not say the data is right. For that you need a schema: one rule for each field.
A rule says the field's type, whether it is required, and sometimes its allowed values. A standard called JSON Schema writes such rules in a fixed format. Here you write your own checklist, in code.
Worked example: a schema for a book
| Field | Type | Required | Allowed values |
|---|---|---|---|
| title | string | yes | any text |
| pages | integer | yes | any whole number |
| genre | string | yes | fiction, history, science |
| rating | number | no | any number |
The cells students fill in are left blank in this preview.
Collect every fault, not only the first
A check that stops at the first fault tells you little. Collect every fault in a list, one short text each, such as pages: wrong type.
The list has two uses. It decides whether to accept the record, and the next lesson sends it back to the model as feedback.
Write the model helper
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(\"status \" + res.status);' ' return (await res.json()).choices[0].message.content;' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + 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"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the schema check, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' '// A schema is a plain checklist: one rule for each field.' 'const schema = [' ' { name: \"title\", type: \"string\", required: true },' ' { name: \"pages\", type: \"integer\", required: true },' ' { name: \"genre\", type: \"string\", required: true, allowed: [\"fiction\", \"history\", \"science\"] },' ' { name: \"rating\", type: \"number\", required: false },' '];' > check2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the schema check, part 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"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the schema check, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Collect every fault, not just the first one.' 'function faults(record, schema) {' ' if (isRecord(record) === false) return [\"not an object\"];' ' const found = [];' ' for (const rule of schema) {' ' if (Object.prototype.hasOwnProperty.call(record, rule.name) === false) {' ' if (rule.required) found.push(rule.name + \": missing\");' ' continue;' ' }' ' const value = record[rule.name];' ' if (typeOk(value, rule.type) === false) found.push(rule.name + \": wrong type\");' ' else if (rule.allowed && rule.allowed.includes(value) === false) found.push(rule.name + \": not allowed\");' ' }' ' return found;' '}' >> check2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the schema check, part 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; }' '}' >> check2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the schema check, part 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const records = [' ' { title: \"Atlas of Rivers\", pages: 240, genre: \"science\" },' ' { title: \"Tide Tables\", pages: \"180\", genre: \"history\" },' ' { title: \"Moon Letters\", pages: 96, genre: \"poetry\", rating: 4.5 },' ' { pages: 310, genre: \"fiction\", rating: \"good\" },' '];' 'records.forEach(function (record, at) {' ' const found = faults(record, schema);' ' console.log(\"Record \" + (at + 1) + \":\", found.length === 0 ? \"ok\" : found.join(\", \"));' '});' >> check2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the schema check, part 6
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const messages = [{ role: \"user\", content: \"Invent one book for a library. Reply with a JSON object with the keys title (text), pages (a whole number) and genre (one of fiction, history, science). Reply with only the JSON.\" }];' ' // Recorded sample, used only when no model answers. It breaks two rules on purpose.' ' const sample = JSON.stringify({ title: \"The Quiet Harbour\", pages: \"210\", genre: \"novel\" });' ' const reply = await ask(messages, sample, 100);' ' const record = pullJson(reply);' ' const found = faults(record, schema);' ' console.log(\"Model record:\", JSON.stringify(record));' ' console.log(\"Faults:\", found.length === 0 ? \"none\" : found.join(\", \"));' '}' 'main();' >> check2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the check
docker exec -w /home/node/project agent node check2.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
One possible output
| Line | What it shows |
|---|---|
| Record 1 | ok |
| Record 2 | pages: wrong type |
| Record 3 | genre: not allowed |
| Record 4 | title: missing, rating: wrong type |
| Model record and Faults | Differ each run: a record from the model, then its faults or none |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think "180" and 180 are the same thing. They look alike, but a program cannot add or compare a text as a number. Check the type, not how the value looks.