Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

How to validate JSON against a schema

After this lesson: I can check a JSON record against a schema and list every fault.

60 min lesson8 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

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

WordWhat it meansExample
Schemaa checklist of the fields a record must have
Typethe kind of value a field holdsinteger
Requireda field that must be present
Allowed valuesthe only values a field may takefiction, history, science
Faultone way a record breaks a rule
Recordone 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

FieldTypeRequiredAllowed values
titlestringyesany text
pagesintegeryesany whole number
genrestringyesfiction, history, science
ratingnumbernoany 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

Command
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

Command
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

Command
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

Command
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

Command
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

Command
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

Command
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

Command
docker exec -w /home/node/project agent node check2.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

One possible output

LineWhat it shows
Record 1ok
Record 2pages: wrong type
Record 3genre: not allowed
Record 4title: missing, rating: wrong type
Model record and FaultsDiffer 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.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.