Prompting & Structured Output · 2 to 3 hours
Validate JSON output and recover from failures
Implement a dependency-free validator for a ticket contract, classify each fixture failure by layer, and design a two-attempt recovery that never commits invalid output.
3 h
Overview
Implement a reliable boundary between model output and application data. You will define a concrete contract, separate validation layers, classify failures, and design bounded recovery. Every exercise uses synthetic fixtures and runs without paid model access.
Study time: 2 to 3 hours
Interview format: Hands-on validation and debugging with synthetic fixtures
By the end you can
- Specify an output contract precisely, including missing, repeated, multiple, and malformed order IDs
- Separate response-state, parsing, structure, source, semantic, and policy checks
- Implement a dependency-free validator and classify each fixture failure by layer
- Design a bounded repair and retry process that terminates without committing invalid output
- Explain which outputs a validator accepts but cannot prove correct, and how evaluation measures them
Prerequisites
Helpful background: Basic JSON and TypeScript knowledge helps.
Start with a precise application contract
Continue the ticket task with exactly two fields, category and order_id. Allowed categories are billing, technical, and other. The category rules remain those from the Easy level: billing covers charges, invoices, and refunds; technical covers application errors and access failures; other covers everything else, too little information, or a ticket with both kinds of issue.
A valid order ID has the exact shape ORD- followed by three digits. Treat it as a complete identifier, so ORD-1234 does not contain a valid order ID for this exercise.
When no ID appears, order_id is null. When multiple distinct IDs appear, category is other and order_id is null. When exactly one distinct ID appears, it must be copied. Repeated occurrences of the same ID still represent one ID.
Express structural rules
The following is an application-side JSON Schema. Adapt its supported subset separately for any provider generation interface.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"category": {"type":"string","enum":["billing","technical","other"]},
"order_id": {"type":["string","null"],"pattern":"^ORD-[0-9]{3}$"}
},
"required": ["category","order_id"],
"additionalProperties": false
}In JSON Schema, naming a property does not require its presence. The required list establishes that obligation. Additional properties are allowed by default unless restricted. String constraints apply to string values, so this pattern does not invalidate null. Check the selected validator's schema-draft support.
Separate validation layers
| Layer | Question | Failure example |
|---|---|---|
| Response state | Did generation complete with a usable result? | Refusal or interrupted stream |
| Parsing | Is there exactly one usable JSON document? | Trailing comma |
| Structure | Are fields, types, and enums valid? | category is urgent |
| Source checks | Are copied values supported by the input? | Invented order ID |
| Task semantics | Does classification follow the policy? | An invoice request labeled other |
| Application policy | Is this operation permitted? | A classification used to authorize a refund |
A validated ticket classification can be stored or routed according to application policy. It does not itself authorize financial or account actions.
A dependency-free validator
This exercise implements the small teaching contract explicitly. It is a focused alternative to importing a schema library. In a larger system, use a maintained validator and keep types aligned with the authoritative schema.
The function returns a discriminated result: the accepted ticket, or the first layer that failed.
type Ticket = {
category: "billing" | "technical" | "other";
order_id: string | null;
};
type ValidationResult =
| { ok: true; value: Ticket }
| { ok: false; kind: "parse" | "schema" | "source"; reason: string };
// Checks run in this order and stop at the first failure:
// parse: raw is valid JSON
// schema: an object with exactly category and order_id, an allowed category,
// and an order_id that is null or ORD- followed by three digits
// source: order_id follows the ID policy for the IDs found in source
declare function validateTicket(raw: string, source: string): ValidationResult;TypeScript types are erased at runtime, so a type assertion alone would not validate external data. The function has to inspect the parsed value field by field before it can return a Ticket.
Code exercise
Ticket output validator
Implement validateTicket(raw, source) for the ticket contract. Return ok with the parsed ticket, or the first failing layer: parse when the text is not JSON, schema when the fields, types, or values break the contract, and source when order_id does not follow the ID policy for the ticket text.
Limitations
This validator implements one small contract by hand. It does not read provider response states, reject duplicate JSON keys (JSON.parse keeps the later value, so use a parser that detects duplicates if they must be rejected), or handle Unicode and punctuation rules a production identifier may need. In a larger system, use a maintained schema validator for the structural layer.
Handle failure states before parsing
Read provider response metadata first. A refusal is a distinct response state. A truncated generation needs diagnosis using output limits and termination metadata. A network failure may leave completion unknown. Do not assume every response body is the requested JSON result.
For streaming, accumulate the complete result and validate it before committing application data. A partial preview can be displayed as provisional UI, but it should not trigger a downstream action.
Bounded repair and retry
Use a small attempt budget, such as two total generation attempts, plus a request deadline and output-size bound. These are teaching defaults, not universal production settings.
For a repairable formatting or schema error, send the original source, the intended contract, and a concise validation error. Revalidate the entire replacement response. A model-generated correction can introduce new mistakes.
A useful repair instruction is "The previous result used an unsupported category. Reclassify the original ticket using billing, technical, or other and return the required object." Keep untrusted prior output and source material labeled as data.
Do not repair missing facts by inventing them. A null or review state may be the correct result. Do not repeatedly retry a refusal to bypass its meaning. Retry temporary transport failures under a separate bounded policy. Ensure that overlapping retry layers cannot multiply attempts unexpectedly.
Deterministic normalization can be appropriate when explicitly part of the contract, such as trimming permitted surrounding whitespace. Silently converting an unknown category into a valid one can hide an error and change meaning.
Worked fixtures: classify each outcome
Predict the outcome
Classify the worked fixtures
Run each raw output through the layers in order: parse, structure, then source checks. Write the first failing layer, or say that it passes, before you reveal the expected results.
| Source | Raw output | Expected result |
|---|---|---|
| Refund ORD-204. | {"category":"billing","order_id":"ORD-204"} | ? |
| App crashes. | {"category":"technical","order_id":null,} | ? |
| App crashes. | {"category":"urgent","order_id":null} | ? |
| App crashes. | {"category":"technical"} | ? |
| App crashes. | {"category":"technical","order_id":"ORD-204"} | ? |
| Refund ORD-101 and ORD-202. | {"category":"billing","order_id":"ORD-101"} | ? |
| Refund ORD-204. | {"category":"technical","order_id":"ORD-204"} | ? |
Evaluate the prompt and validator together
Build fixtures for ordinary requests, absent fields, mixed categories, duplicate IDs, hostile instructions, and long inputs. Define expected labels before trying prompt variants. Keep a held-out set for confirming changes.
Measure parse validity, schema validity, source-policy success, category accuracy, repair frequency, and end-to-end success. Use explicit denominators. For example, schema validity among parseable outputs can look excellent while many responses fail parsing. Also report successful tasks divided by all attempted tasks.
Version the prompt and contract used in each run. Change one meaningful factor at a time where practical. Keep deterministic validator tests separate from repeated model evaluations.
Completion checklist
- Implement the validator and pass the practice problem's tests.
- Reproduce the seven fixture outcomes, including the one the validator accepts.
- Explain what the validator proves and what it cannot.
- Show how a two-attempt recovery terminates without committing invalid output.
- Report parse, schema, source, and end-to-end success with explicit denominators.
Sources
- Claude structured outputs documentation Schema-constrained output, its JSON Schema limitations, and the refusal and output-limit cases where a reply can still miss the schema.
- Claude stop reasons and fallback Reading the stop reason, including refusals and output-limit truncation, before using a response.
- JSON Schema object reference Object properties, required fields, and additional properties.
- RFC 8259: The JSON Data Interchange Format Object names should be unique, and parsers differ on duplicates; many keep only the last value.
- Anthropic: define success criteria and build evaluations Explicit success criteria and graded test cases for prompt changes.
The ticket contract, fixtures, validator, attempt budget, and rubric are original synthetic teaching material for a fictional support product. The two-attempt budget is a teaching default, not a production recommendation. Study times and readiness thresholds are editorial estimates for self-assessment. Response states, stop reasons, and supported schema keywords differ between providers, so check the selected service's current documentation.
Related on CodeSnatch
Lessons, problems, and articles that build the skills this question uses.
AI Engineering
Builds a pure retry policy with attempt limits and a deadline check, the core of a bounded recovery loop.
AI Engineering
Reads stop reasons and token budgets to tell a cut-off reply from a finished one before any parsing.
Code snippet
Derives types from one runtime schema and wraps parsing in a Result, as a maintained validator would.
Practice problem
Report each rate with its own denominator and keep missing outputs counted, as prompt evaluation needs.
Question bank
Contrasts anchored whole-string regex checks with unanchored searches, useful for ID format and ID lookup.
Practice the interview question
Answer it in your own words before you look at the reference answer.
