Prompting & Structured Output · 60 to 90 minutes
Validate a model's JSON review summary
Write a pure function that accepts a model's JSON review summary or returns every contract violation as a stable error code, predict its result for eight replies, and say what a valid summary still cannot prove.
Live coding
1.5 h
Overview
Practice the Easy live-coding task: a small JSON contract, a handful of model replies, and a pure validator that accepts or rejects each one with error codes. The scenario is a fictional bookshop that summarizes customer reviews for a moderation dashboard, and every exercise runs without an API account. This level checks structure only; checking values against the source text and asking the model again after a failure belong to the Medium level, Validate JSON output and recover from failures.
Study time: 60 to 90 minutes
Interview format: Live coding against a JSON contract, 45 to 60 minutes
By the end you can
- Restate an output contract and settle its unclear cases (null versus missing, unknown keys, letter case, whole-number floats) before writing code
- Check runtime JSON types in JavaScript and Python, including numeric strings, fractions, booleans, null, and arrays
- Implement a pure validator that returns ordered error codes for every failing field and never throws on bad model output
- Write table-driven tests for valid, invalid, boundary, and cross-language replies
- Explain what a structurally valid summary can still get wrong and what happens to a rejected one
Prerequisites
Helpful background: Basic JavaScript or Python is enough; no API account is needed.
The review summary feature
Worked example
Suppose an online bookshop that sells new and secondhand books asks a language model to summarize each written customer review as JSON. A moderation dashboard shows the summary next to the review, lets moderators filter reviews by sentiment and topic, and averages the rating per book. A program reads every reply, so a reply that breaks the contract must never reach the database or the dashboard.
The replies are untrusted text: most follow the contract and some do not. The three fields mean:
rating: the model's estimate, from 1 to 5, of how satisfied the reviewer issentiment: the overall tone of the reviewtopics: up to three things the review discusses, fromprice,delivery,condition(the physical copy), andstory(the book's content)
For example, the review "Arrived two weeks late and the spine was cracked." gets this reply, and the contract accepts it:
{"rating": 2, "sentiment": "negative", "topics": ["delivery", "condition"]}The contract and the result
A reply is accepted only when it is a JSON object with exactly these three keys:
| Field | Rule |
|---|---|
rating | An integer from 1 to 5 inclusive. An integer is a JSON number with no fractional part, so 4.0 counts as 4. Booleans, strings, null, and fractions are not integers. |
sentiment | Exactly one of "positive", "mixed", "negative" (case-sensitive, no trimming) |
topics | An array of 0 to 3 items, each exactly one of "price", "delivery", "condition", "story", with no item repeated |
| Any other key | Not allowed |
The validator runs these checks in this order:
| Step | Code | Fails when |
|---|---|---|
| 1 | reply:parse | The text is not valid JSON |
| 2 | reply:type | The parsed value is not a plain object (an array, null, a string, a number, or a boolean) |
| 3a | rating:missing | The key is absent (a key present with the value null is not missing) |
| 3b | rating:type | The value is not an integer as defined above |
| 3c | rating:range | The integer is below 1 or above 5 |
| 4a | sentiment:missing | The key is absent |
| 4b | sentiment:type | The value is not a string |
| 4c | sentiment:value | The string is not exactly one of the three values |
| 5a | topics:missing | The key is absent |
| 5b | topics:type | The value is not an array |
| 5c | topics:too-many | The array has more than 3 items |
| 5d | topics:value | Any item is not exactly one of the four topics (one code, however many items fail) |
| 5e | topics:duplicate | Every item is allowed, but one appears more than once |
| 6 | extra:<key> | One code per key the contract does not name, with the key exactly as written |
- Steps 1 and 2 end validation at once: the result holds only that code.
- Steps 3, 4, and 5 each add at most one code: the first rule in the listed order that fails for that field. All three fields are always checked.
- Field codes come first in contract order (rating, sentiment, topics), then the extra codes, sorted by plain string comparison (JavaScript's default
sort(), Python'ssorted()). Only the extra codes are sorted. - No code means the reply is accepted.
The result, in TypeScript terms:
type ReviewSummary = {
rating: number; // an integer from 1 to 5
sentiment: "positive" | "mixed" | "negative";
topics: ("price" | "delivery" | "condition" | "story")[];
};
type ValidationResult =
| { ok: true; value: ReviewSummary }
| { ok: false; errors: string[] };
declare function validateReviewSummary(raw: string): ValidationResult;Python's validate_review_summary(raw) returns a dict with the same keys, using lists, never tuples. An accepted value is a new object with its keys in contract order, rating as an integer (a Python int, also for an input such as 4.0), and topics in the reply's order. A failed result has no value key, and an accepted one has no errors key.
Check the type the parser gave you
A TypeScript type or a Python annotation checks nothing at runtime: after parsing, a field can hold anything JSON allows, and the two parsers disagree on some types.
| JSON text | JavaScript after JSON.parse | Python after json.loads |
|---|---|---|
4 | number 4 | int 4 |
4.0 | number 4 | float 4.0 |
4.5 | number 4.5 | float 4.5 |
"4" | string | str |
true | boolean | bool (a subclass of int) |
null | null (typeof gives "object") | None |
[] | array (typeof gives "object") | list |
{} | object | dict |
- In JavaScript,
typeofreturns "object" for null and for arrays, so a plain-object check also needsvalue !== nulland!Array.isArray(value).typeof 4.5is "number", so useNumber.isInteger, which rejects fractions, strings, and booleans. - In Python,
boolis a subclass ofint, soisinstance(True, int)isTrueandTrueequals 1. Reject booleans before you check for an integer. - The contract defines an integer as a number with no fractional part, as JSON Schema does, so both languages accept 4.0. In Python,
float.is_integer()tests it andint(4.0)gives the value to return. - A key with the value null is present; an absent key is missing. Separate codes keep the causes apart: the model left the field empty, or it dropped the field.
Reject keys the contract does not name
JSON Schema allows properties a schema does not list, unless additionalProperties is false. For model output, rejecting them is the safer default:
- A field nobody reviewed, such as a
reply_to_customerthe model invents, could otherwise reach storage or the dashboard. - A new key signals that the prompt or the model changed.
- A key in the wrong letter case shows up twice, as a missing field and as an extra key, which points at the cause.
Adding a field later is a contract change: the prompt, the validator, and every consumer change together under a new contract version (the Hard level, "Design a versioned output contract and migration").
Return codes, not messages
Each error is a short code shaped <subject>:<rule>, such as rating:range, whose wording never changes. Tests assert exact codes, logs count them per prompt and contract version to show which rule the model breaks most, the dashboard maps each one to a message for moderators, and a later corrective request to the model could quote them. Prose messages change with every edit and break whatever reads them.
Every failing field, in one pass. The three fields are independent: a bad rating says nothing about the topics. So one pass reports each failing field once, with its first failing rule, and one log line shows everything wrong with a reply. A failure of the whole reply still stops at once, because there is no object to check.
Compared with the Medium level. "Validate JSON output and recover from failures" checks a ticket reply in dependent layers, so it stops at the first failing layer and then designs a bounded retry; this level has one structural layer and reports every failing field.
Bad model output is an expected input, not a bug in your program, so the validator returns codes instead of throwing.
One reply, rule by rule
Worked example
Take this reply, whose keys arrive in an unusual order:
{"topics": ["price", "price"], "summary": "Arrived late", "sentiment": "neutral", "rating": 3}| Step | Check | Outcome |
|---|---|---|
| 1 | Parse, then plain object | It parses to an object, so validation continues |
| 2 | rating: present, integer, 1 to 5 | 3 passes, no code |
| 3 | sentiment: present, string, allowed value | "neutral" is a string but not an allowed value: sentiment:value |
| 4 | topics: present, array, at most 3, allowed values, distinct | Two items, both allowed, but "price" appears twice: topics:duplicate |
| 5 | Extra keys, sorted | summary is not in the contract: extra:summary |
The result is { ok: false, errors: ["sentiment:value", "topics:duplicate", "extra:summary"] }. The reply lists topics first, but the codes follow the contract order: rating, sentiment, topics, then the extra keys.
A second reply: {"topics": ["delivery"], "sentiment": "mixed", "rating": 3.0}. Every rule passes, and 3.0 has no fractional part, so it counts as the integer 3. The result is { ok: true, value: { rating: 3, sentiment: "mixed", topics: ["delivery"] } }, and Python returns the int 3. The value is rebuilt in contract order instead of returned as parsed: the same accepted content always serializes the same way, and nothing the validator did not check can pass through.
Predict the validator's results
Apply the contract by hand before you write any code. Each reply is the exact text the validator receives (R2 is described in words because it contains a code fence). The practice problem's first test lists the expected results, so predict R1 to R8 before you open it.
Predict the outcome
Predict the validator's results
Apply the contract to each reply. Write the accepted value, or the error codes in order, before you reveal the answers.
| Reply | Raw reply text | Expected result | Deciding rules |
|---|---|---|---|
| R1 | {"rating": 4, "sentiment": "positive", "topics": ["story", "condition"]} | ? | ? |
| R2 | The object {"rating": 5, "sentiment": "positive", "topics": ["delivery"]} inside a Markdown code fence: the line ```json before it and the line ``` after it | ? | ? |
| R3 | [{"rating": 3, "sentiment": "mixed", "topics": []}] | ? | ? |
| R4 | {"rating": "4", "sentiment": "Positive", "topics": ["price"]} | ? | ? |
| R5 | {"rating": 4.5, "sentiment": null, "topics": "delivery"} | ? | ? |
| R6 | {"rating": true, "topics": ["price", "delivery", "condition", "story"]} | ? | ? |
| R7 | {"rating": 6, "sentiment": "negative", "topics": ["delivery", "shipping", "delivery"], "priority": "high", "flagged": true} | ? | ? |
| R8 | {"rating": 5, "sentiment": "negative", "topics": []} | ? | ? |
Write the tests first, as a table
In a live-coding round, settle the contract before the code. Restate it (the three fields, the two result shapes, the order of the codes), then ask about the unclear cases: is null the same as missing, may topics be empty, does letter case matter, is 4.0 an integer, and what happens to an unknown key? This contract answers all five; in an interview, ask. Then write the tests as a table of replies and expected results, before the function:
| Row group | What it pins down |
|---|---|
| A valid reply | The accepted value, its key order, and the order of the topics |
| One row per rule | Each code alone, so a failing test names the rule it broke |
| Both sides of every bound | The lowest and highest allowed rating and one value past each, the largest allowed number of topics and one more, and an empty array |
| Combined failures | Two or three failing fields in one reply, so the order of the codes is tested |
| Early exits | Empty text, a fenced reply, a reply cut off mid-string, prose, and JSON that is not an object |
| Cross-language rows | A boolean and a whole-number float as the rating, plus a numeric string (a loose range check accepts it in JavaScript and crashes in Python) |
| Key names | An unknown key, several unknown keys out of order, and a key in the wrong letter case |
Keep the tests readable as the contract grows: one group per field, test names that state the rule ("a rating of 0 is out of range"), and a helper that builds a valid reply and changes one field, so each row shows only what differs.
Implement the validator
Implement the contract in JavaScript or Python. Write your test table first, then the function, and run the practice problem's tests.
Code exercise
Review summary validator
Implement validateReviewSummary(raw) (Python validate_review_summary(raw)) for the review summary contract. Return the accepted value, or every failing field's first failing code in contract order followed by the sorted extra keys. Never throw for bad model output.
Expected
The linked practice problem calls validateReviewSummary(raw) in JavaScript and validate_review_summary(raw) in Python; the result keys are the same in both languages. An accepted reply returns { ok: true, value }, where value is a new object with exactly rating, sentiment, and topics in that order, rating is an integer (a Python int, also for an input such as 4.0), and topics keeps the reply's order. A rejected reply returns { ok: false, errors } with the codes in the stated order. The function never throws. The six tests per language check: replies R1 to R8 in one comparison; text that is not JSON (empty, fenced, cut off, prose) and JSON that is not an object (an array, null, a string, a number, true); rating and sentiment edges (fractions, numeric strings, booleans, null, a missing key, both sides of each bound, letter case, a trailing space); topics edges (an empty array, three and four items, duplicates, unknown, wrongly cased, and non-string items, a string, null, an object, and a missing key); extra keys sorted after the field codes, including integer-like and wrongly cased names, and {} with all three fields missing; and the result shapes (the accepted value's key order, 4.0, a reply padded with whitespace, results that later calls do not share, and a failed result with only ok and errors).
Limitations
This validator checks structure only. It does not read provider response states such as a refusal or a truncated reply, does not reject repeated keys (both parsers keep the last value), and does not treat NaN and Infinity the same way in both languages (Python's json.loads accepts them by default; pass parse_constant to reject them). It cannot judge whether the rating, sentiment, or topics match the review. A larger contract is easier to keep correct with a maintained JSON Schema validator whose errors are mapped to these codes.
Review two ports of the validator
A teammate's validator is easiest to review with the fixtures in hand: run each reply through their code in your head and compare the result with the contract.
Diagnose
Review two ports of the validator
Two teammates ported the validator before the contract was final. Port A (JavaScript) and Port B (Python) match the contract except for the checks below. has(key) checks for an own key, and TOPICS is the list of four allowed topics.
// Port A (JavaScript): rating and topics checks
if (!has('rating')) errors.push('rating:missing');
else if (typeof data.rating !== 'number') errors.push('rating:type');
else if (data.rating < 1 || data.rating > 5) errors.push('rating:range');
const topics = data.topics;
if (!has('topics')) errors.push('topics:missing');
else if (topics.length > 3) errors.push('topics:too-many');
else if (!topics.every((topic) => TOPICS.includes(topic))) errors.push('topics:value');
else if (new Set(topics).size !== topics.length) errors.push('topics:duplicate');# Port B (Python): rating check
if "rating" not in data:
errors.append("rating:missing")
elif not isinstance(data["rating"], int):
errors.append("rating:type")
elif not 1 <= data["rating"] <= 5:
errors.append("rating:range")- Which of R1 to R8 does each port get wrong, and what does it return instead?
- What does each port do with
{"rating": 3, "sentiment": "mixed", "topics": null}and with{"rating": 4.0, "sentiment": "mixed", "topics": []}? - Give a one-line fix for each defect and a test that would have caught it.
What validation cannot prove
A reply can pass every rule and still misread the review: sarcasm ("Great, the book arrived in pieces") summarized as positive, story on a review that mentions only the price, or a rating of 1 next to positive sentiment. The structure is right, so the validator accepts each one. Other checks catch them: an evaluation set of labeled reviews (the Evaluation and Observability levels), moderator spot checks, or a consistency rule the contract states explicitly, such as the ratings each sentiment allows, which then becomes one more code.
Validate every reply before it is stored or displayed, and log each rejection's codes with the prompt and contract version. Asking the model again is a separate choice with a small attempt budget and a deadline; the Medium level designs it.
Provider structured outputs reduce parse and type failures but do not remove them: one provider documents that refusals and token-limit cut-offs can miss the schema, that enum capitalization can differ, and that minimum and maximum are not enforced. This contract compares sentiment exactly, by choice; declaring lowercase normalization before coding is also valid.
Completion checklist
- I can restate the review contract and settle null versus missing, unknown keys, letter case, and 4.0 before coding.
- I can check runtime JSON types in JavaScript or Python, including booleans, fractions, null, and arrays.
- I can return each failing field's first code in contract order, then the sorted extra keys, without throwing.
- I can write a test table that covers every rule, both sides of each bound, the early exits, and cross-language replies.
- I can say what happens to a rejected reply and what an accepted summary still cannot prove.
Sources
- RFC 8259: The JSON Data Interchange Format Section 6: NaN and Infinity are not permitted, and a number may carry a fraction and an exponent, so 4 and 4.0 are both plain JSON numbers. Section 4: object names should be unique, and parsers behave differently when a name repeats.
- JSON Schema 2020-12: Validation vocabulary Section 6.1.1: the integer type matches any number with a zero fractional part. Also defines enum, maxItems, uniqueItems, and required.
- Understanding JSON Schema: object Additional properties are allowed by default, and additionalProperties set to false rejects them; listed properties are not required unless named in required.
- Python json module json.JSONDecodeError is a subclass of ValueError; a JSON number with a fraction becomes a float; NaN, Infinity, and -Infinity are accepted by default and parse_constant can reject them; a repeated name keeps the last value.
- Python built-in types: Boolean type bool is a subclass of int, and False and True behave like 0 and 1 in numeric contexts. The same page documents float.is_integer().
- Claude structured outputs documentation One provider's example. Invalid outputs: a refusal or a reply cut off at max_tokens can miss the schema, and enum capitalization is not guaranteed, so compare enum values case-insensitively. JSON Schema limitations: numerical constraints such as minimum and maximum are not supported, and additionalProperties must be false.
The bookshop, review contract, replies, error codes, teammate ports, and rubric are original synthetic teaching material about a fictional application. Study times and readiness thresholds are editorial estimates for self-assessment. Product, protocol, and model details are as of November 2026.
Related on CodeSnatch
Lessons, problems, and articles that build the skills this question uses.
Question bank
Drills Array.isArray checks and Set-based dedupe, two JavaScript moves the type and duplicate rules need.
DSA lesson
Practice stepping through code and recording state, the skill behind predicting each reply's result by hand.
AI Engineering
Scores outputs against labeled cases, the kind of check that catches summaries a structural validator accepts.
System Design
Its error format pairs machine-readable codes with field-level details, the idea behind stable error codes.
Community article
Explains why Python annotations are never enforced at runtime, so every parsed field needs its own check.
Practice the interview question
Answer it in your own words before you look at the reference answer.
