The reply that broke the app
Asking a language model for JSON means telling it, in the prompt, the exact shape of the answer you want. It matters because a program can only use a reply it can read.
A school shop app asks a model to invent a snack and a price. One day the reply starts with "Sure, here you go" and the program stops. The model did what you asked, but not in a shape the program could read.
By the end of this lesson
Note
I can ask for an answer in a shape my program can read.
Key words
| Word | What it means | Example |
|---|---|---|
| Prompt | the text you send to a model | |
| JSON | a text format for data, made of keys and values | |
| Key | the name of one field in a JSON object | price |
| Code fence | three backticks that wrap code in a reply | |
| Parse | to read text into data a program can use | |
| Token limit | the most pieces of text a reply may have |
The cells students fill in are left blank in this preview.
Say the shape in the prompt
A model guesses what you want from your words. So say the shape. Name the keys, say the type of each value and say what to leave out.
End with one plain rule: reply with only the JSON. The more you leave to chance, the more different replies you will get.
Worked example: three prompts for one snack
| The prompt says | What may come back |
|---|---|
| Give me a snack and a price | Plain words, maybe a list, no fixed keys |
| Give me a snack as JSON | Probably JSON, with keys you did not choose |
| Reply with a JSON object with the keys name (text) and price (a number). Reply with only the JSON. | More likely to be the shape you asked for |
The cells students fill in are left blank in this preview.
Show one example answer
An example is worth a paragraph of rules. Put one complete example answer in the prompt, with made-up values, and say the real answer must have the same keys.
Models tend to copy the layout of an example. Use values that cannot be mistaken for real ones, or a copied example looks like a real answer.
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 first script, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'const messages = [' ' { role: \"system\", content: \"You answer with JSON only.\" },' ' { role: \"user\", content: \"Invent one snack for a school shop. Reply with a JSON object with the keys name (text) and price (a number in pounds). Reply with only the JSON.\" },' '];' '// Recorded sample, used only when no model answers. It is messy on purpose.' 'const fence = \"\x60\x60\x60\";' 'const sample = \"Sure, here is your snack:\n\" + fence + \"json\n\" + JSON.stringify({ name: \"Oat flapjack\", price: 1.2 }) + \"\n\" + fence + \"\nHope that helps.\";' > ask1.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the first script, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function pullJson(text) {' ' try { return JSON.parse(text); } catch (error) { }' ' 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; }' '}' '' 'async function main() {' ' const reply = await ask(messages, sample, 100);' ' console.log(\"Raw reply:\");' ' console.log(reply);' ' let whole = true;' ' try { JSON.parse(reply); } catch (error) { whole = false; }' ' console.log(\"The whole reply is JSON:\", whole ? \"yes\" : \"no\");' ' console.log(\"Pulled out:\", JSON.stringify(pullJson(reply)));' '}' 'main();' >> ask1.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run it and read the reply
docker exec -w /home/node/project agent node ask1.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 may show |
|---|---|
| Raw reply | A sentence, then the JSON in a fence, then another sentence |
| The whole reply is JSON | no |
| Pulled out | {"name":"Oat flapjack","price":1.2} |
The cells students fill in are left blank in this preview.
Why a good prompt can still fail
Even a clear prompt can fail. The model may add a greeting, wrap the JSON in a fence, or change a key name. It may also stop early if the token limit is too small, which leaves JSON cut off half way.
So you always parse and check. Asking well makes a bad reply rarer. It never makes one impossible.
A common mistake
Warning
Many people think a clear enough prompt means they can skip the check. A prompt only lowers the chance of a bad reply. Keep the check even when the last hundred replies were fine.