Validate structured output
When your program asks a model for data, the reply is text. Even when you ask for JSON, the reply can be cut off, miss a field, or hold a value you never allowed. If your code trusts it, one odd reply breaks the program or, worse, quietly does the wrong thing. So treat a model's reply like input from a stranger: parse it, check its shape, its types and its allowed values, and accept it only when everything matches.
Learn it
Words to know
- JSON
- A text format for data: objects in
{ }, lists in[ ], strings, numbers,true,falseandnull. - structured output
- A model reply in a fixed data format, such as JSON with agreed fields, so that a program can read it.
- schema
- The agreed shape of a piece of data: which fields it has, their types, and which values are allowed.
How it works
- Parse the text as JSON. If parsing fails, the reply is invalid.
- Check the shape: the value must be an object with exactly two keys.
null, a list, a number, a string,trueandfalseare valid JSON too, and all of them are rejected. - Check each value:
categoryis one ofbilling,technicalandother;confidenceis a number from 0 to 1, both included. - Answer true only if every check passed. Never raise an error: a bad reply is an answer of false.
Worked example
Input text = '{"category":"billing","confidence":"0.8"}' Output false
| Check | What the reply has | Passes? |
|---|---|---|
| Parses as JSON | complete, well-formed JSON text | yes |
| Is an object | an object, not a list or null | yes |
| Exactly two keys | category and confidence | yes |
| Category is allowed | billing | yes |
| Confidence is a number | the text "0.8", in quotes | no |
Four checks pass and the last one fails, so the reply is rejected. A reply is valid only when every check passes.
Why it matters
This is the gate between a model and the rest of your program. The second roadmap milestone builds a ticket classifier around this check, and tests it with saved replies, valid and broken.
Your turn
Decide whether a model's reply is a valid support-ticket classification.
text- the model's reply, as a string; it may not be JSON at all
- Returns
- True if the reply is a JSON object with exactly the keys
categoryandconfidence, an allowed category, and a confidence that is a number from 0 to 1, both included. Otherwise false; in Python these areTrueandFalse. It must never raise an error.
What JavaScript gives you
JSON.parse(text)- turns JSON text into a value; it throws an error on broken text
try { } catch { }- runs code, and catches an error if one is thrown
Array.isArray(x)- true only for a list
Object.keys(x).length- how many keys an object has
What Python gives you
json.loads(text)- turns JSON text into a value; on broken text it raises
json.JSONDecodeError, a kind ofValueError try: ... except ValueError: ...- runs code, and catches that error
isinstance(x, dict)- true only for a JSON object
x.get("key")- the value under a key, or
Nonewhen the key is missing;x["key"]raisesKeyErrorinstead type(x) in (int, float)- true for a number, and not for
TrueorFalse
Example 1
Input text = '{"category":"billing","confidence":0.8}' Output true
Valid JSON, both keys, an allowed category, and a confidence between 0 and 1.
Example 2
Input text = '{"category":"billing","confidence":"0.8"}' Output false
The confidence is the text "0.8", not a number. A value of the wrong type is rejected, even if it looks right.
Example 3
Input text = '{"category":"admin","confidence":0.8}' Output false
admin is not one of the three allowed categories.
Example 4
Input text = "not json" Output false
The text does not parse at all. The function answers false instead of raising an error.
The checks also try
- a confidence of exactly 0
- a confidence of exactly 1
- a confidence above 1
- a confidence below 0
- an extra key
- the JSON value null
- a list instead of an object
- true as the confidence
- a bare number
- a list as the category
- a list that holds a category and a number
- two keys, one with the wrong name
- null as the confidence
- the two keys in the other order
- a category with a capital letter
Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1: Where to start
Parsing can fail, so it has to happen inside the language's block for catching errors. If it fails, the answer is false.
Hint 2: The key step
Check the shape before the values. A parsed reply can be null, a list, a number or a string, and none of those has the two keys to look at.
Hint 3: The outline
Parse inside a try block, and return false if that raises an error. Return false unless the value is an object, not null and not a list, with exactly two keys. Return false unless
categoryis one of the three names. Return true only ifconfidencehas the type number and lies from 0 to 1, both included.
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
Step by step
- Parse the text; a reply that is not JSON is invalid. Parsing raises an error on broken text. Catching it turns the error into the answer false, as the task requires.
- Only an object with exactly two keys can be right.
null, lists, numbers and strings are all valid JSON, and none of them is a classification. An extra key means the model added something you did not ask for. - The category must be one of the three names. A model can invent a label such as
admin. The code after this point only knows how to handle the three names you defined. - The confidence must be a number from 0 to 1. The text
0.8in quotes and the valuetrueare not numbers. Checking the type first keeps the range check from comparing the wrong kind of value.
The same numbered steps are the comments in the solution.
JavaScript
function solve(text) {
// 1. Parse the text; a reply that is not JSON is invalid
let reply;
try {
reply = JSON.parse(text);
} catch {
return false;
}
// 2. Only an object with exactly two keys can be right
if (reply === null || typeof reply !== "object") return false;
if (Array.isArray(reply) || Object.keys(reply).length !== 2) return false;
// 3. The category must be one of the three names
const categories = ["billing", "technical", "other"];
if (!categories.includes(reply.category)) return false;
// 4. The confidence must be a number from 0 to 1
const confidence = reply.confidence;
if (typeof confidence !== "number") return false;
return confidence >= 0 && confidence <= 1;
}Python
import json
def solve(text):
# 1. Parse the text; a reply that is not JSON is invalid
try:
reply = json.loads(text)
except ValueError:
return False
# 2. Only an object with exactly two keys can be right
if not isinstance(reply, dict) or len(reply) != 2:
return False
# 3. The category must be one of the three names
if reply.get("category") not in ("billing", "technical", "other"):
return False
# 4. The confidence must be a number from 0 to 1
confidence = reply.get("confidence")
if type(confidence) not in (int, float):
return False
return 0 <= confidence <= 1
Watch out
- Valid JSON is not a valid reply.
null,[]and5all parse without an error, so parsing alone proves nothing about the shape. - Do not convert values to make them fit. Turning the text
0.8into a number hides the fact that the model broke the agreement. - In JavaScript: In JavaScript
typeof nullisobject, and a list is an object too. Rule out both before you look at the keys. - In Python: In Python
Truecounts as the number 1:isinstance(True, int)is true. Compare the exact type, so a boolean is not accepted as a confidence.
Check yourself
Why is it not enough that a reply parses as JSON?
Parsing only proves the syntax. The value can still be null, a list, an object with missing or extra keys, or hold values of the wrong type.
What should the function do with a reply it cannot parse?
Answer false. A bad reply is an expected case, so it gets an answer, not an error that stops the program.