Topic: Learning notes
Structured Output: The AI Returned JSON. Now What?
Compare format prompts, JSON mode, and schema constraints. Use metadata checks, timestamps, and a token-estimation counterexample to see what the application still needs to validate.
“Return only JSON. No explanation.”
If the next step is JSON.parse(), an extra “Sure, here’s your data” can send that helpful response straight into catch. The reply is polite. The application is probably less appreciative.
Following Core Prompting Techniques, this note covers Sabrina Goldfarb’s Structured Output lesson. I’ll connect the requested format to the checks needed after receiving it, using meeting extraction and prompt-library metadata.
The prompts and expected JSON are original examples, not observed model responses. Date and field validation, along with the token-estimation counterexample, have executable checks.
Are we requesting a format or constraining generation?
The course uses structured output in the broad sense of requesting a predictable format. Fixed text fields, document templates, and code blocks can all make subsequent handling clearer. Uses include extraction, test cases, documentation, and batches of similar content.
When connecting an API, distinguish the mechanisms:
| Approach | What it constrains | What the receiver still handles |
|---|---|---|
| Prompt instructions and demonstrations | Guides the model toward a format | Deviations, extra text, and parsing failures |
| JSON mode, where available | Valid JSON, without a specific field structure | Fields, types, and exceptions such as interrupted output |
| API schema-constrained output | Generation structure within supported schema features | Service exceptions, factual accuracy, and business rules |
OpenAI’s guide distinguishes JSON mode from schema adherence. Anthropic’s guide describes constrained decoding and its supported scope. Pasting a schema into a prompt doesn’t activate an API’s constraint mechanism.
Make the extraction request concrete
Start with a small email-extraction prompt:
Extract dateText, timeText, location, and topic from the email below.
Include all four fields. Each value must be a string or null.
Use null for information not mentioned; do not fill it in yourself.
Preserve date and time wording instead of converting to timestamps.
Return only a JSON object, without Markdown fences or explanations.
Email: Let's meet tomorrow at three in the afternoon in Conference Room C
to discuss the new homepage.
The manually specified expected result is:
{
"dateText": "tomorrow",
"timeText": "three in the afternoon",
"location": "Conference Room C",
"topic": "new homepage"
}
This example deliberately keeps “tomorrow.” Resolving it into a timestamp requires the email’s reference date and applicable time zone. The date on which a program happens to run cannot silently supply that missing context.
Next, remove the location or topic and check for null. That separates format compliance from faithful extraction. Beautiful JSON containing a meeting room absent from the email still needs to be rejected.
Schemas name the fields; check who enforces the rules
The course then asks AI to generate metadata functionality for a prompt library, including trackModel, updateTimestamps, and estimateTokens. That specifies requirements for generated code; it doesn’t automatically enable runtime schema constraints.
I’ll take the model name and two timestamps as a smaller example:
{
"model": "example-model-v1",
"createdAt": "2026-10-19T09:00:00.000Z",
"updatedAt": "2026-10-19T09:00:00.000Z"
}
A general JSON Schema for those fields could be:
{
"type": "object",
"properties": {
"model": { "type": "string", "minLength": 1, "maxLength": 100 },
"createdAt": { "type": "string", "format": "date-time" },
"updatedAt": { "type": "string", "format": "date-time" }
},
"required": ["model", "createdAt", "updatedAt"],
"additionalProperties": false
}
This describes required fields, types, and the extra-field policy. It doesn’t compare the timestamps or reject a model name made entirely of whitespace.
The JSON Schema specification bases date-time on RFC 3339; enforcing format depends on the validator and its configuration. APIs also support subsets of keywords. At the time of review, Anthropic’s raw API schema doesn’t support minLength or maxLength; receiving code must enforce them. This general schema isn’t a request configuration to paste unchanged into every API.
A timestamp needs to be a real date, too
The small example uses these rules:
| Field | Rule in this example |
|---|---|
| model | Must contain non-whitespace text, with at most 100 Unicode code points; no fixed model enumeration |
| createdAt, updatedAt | Real dates in fixed UTC millisecond notation, such as 2026-10-19T09:00:00.000Z |
| Ordering | updatedAt must be at or after createdAt |
This deliberately chooses a narrow timestamp representation. It doesn’t claim that every ISO 8601 or RFC 3339 timestamp looks this way. An offset such as +08:00 can represent a valid time, but this example requires callers to normalize it to UTC first.
Here is the date-checking excerpt from the download. data has already passed its object, field, and model-name checks:
for (const field of ["createdAt", "updatedAt"]) {
const value = data[field];
if (
typeof value !== "string" ||
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
!Number.isFinite(Date.parse(value)) ||
new Date(value).toISOString() !== value
) {
throw new Error(`${field} must be a real date in UTC millisecond format`);
}
}
if (Date.parse(data.updatedAt) < Date.parse(data.createdAt)) {
throw new Error("updatedAt must be at or after createdAt");
}
The regular expression checks the shape. Parsing and converting back to the same representation catches date-shaped inputs such as 2027-02-30. Only then do we compare creation and update times; equality is allowed.
Writing JSON.parse(text) as Metadata doesn’t run those checks. The TypeScript handbook explains that assertions disappear at compilation. External data still needs runtime validation. Reuse the project’s existing validation tools when available.
A flexible model name lets records accommodate new versions. If that field chooses a model to actually call, supported-model and authorization checks remain a separate responsibility. Application-known facts, such as the model requested and creation time, should be recorded by the application rather than asking the model to guess its own biography.
Token estimates: check which end of the range is larger
The course’s token estimator uses this heuristic, multiplying both values by 1.3 for code:
min = 0.75 × word_count
max = 0.25 × character_count
Try a b c d: four whitespace-separated words and seven characters including spaces.
| Calculation | Result |
|---|---|
| min = 0.75 × 4 | 3 |
| max = 0.25 × 7 | 1.75 |
| After applying the code multiplier | min = 3.9, max = 2.275 |
We haven’t contacted a model, and the “range” is already upside down. Sorting the two values makes the display look plausible, but doesn’t establish that the actual token count falls between them.
I’d retain 1.3 as a teaching heuristic, not a constant for every programming language, text, or tokenizer. For capacity and cost budgeting, use the relevant model’s tokenizer or provider counting interface, and check the complete request and actual usage.
The course maps high, medium, and low to green, yellow, and red, using 1,000 and 5,000 tokens as band boundaries. An implementation still needs to define which estimate is compared and where boundaries belong. These labels aren’t calibrated probabilities of accuracy. Keep the text labels visible so color isn’t the only signal.
Stop errors where they can be handled
When integrating a service, I’d check completion status before parsing a normal result:
| Layer | Example | Handling direction |
|---|---|---|
| Service and completion status | API errors, refusals, truncated output | Handle the service response separately from normal data |
| JSON syntax | Extra explanation, missing brackets | Report a parsing failure |
| Structure and fields | Missing fields, wrong types, disallowed extras | Report validation failure |
| Content and business rules | Earlier update time, an invented location | Check evidence and application rules |
Anthropic documents refusals and token limits as cases that may not match the schema. An HTTP 200 alone therefore isn’t enough to send the response straight to a database. If retries are appropriate, bound them and define when to stop. A format failure doesn’t justify retrying forever.
Likewise, conveniently formatted code still needs compilation, tests, and relevant security checks. Structured output helps specify a contract without performing those tasks for us.
Run the receiving-side checks
Download the complete validation example and run it with Node.js:
node structured-output-validation.mjs
It uses built-in Node.js assertions with no extra packages. Checks cover valid input, equal timestamps, leap days, invalid dates, reversed ordering, missing and extra fields, blank or overlong model names, and the token-estimation counterexample.
These checks passed locally and verify receiving-side behavior. Whether a model consistently extracts the correct information needs separate testing; this passing result cannot stand in for that evidence.
References and scope
- Sabrina Goldfarb, Structured Output lesson: format requirements and the metadata exercise. The site lists text under CC BY-NC 4.0 and code samples and exercises under Apache 2.0.
- OpenAI, Structured model outputs: JSON mode versus schema constraints.
- Anthropic, Structured outputs: supported schema features, SDK validation, and response exceptions.
- JSON Schema, Validation specification: field validation and format behavior.
- TypeScript, Type Assertions: runtime limitations of assertions.
- Anthropic, Token counting: input counts for the specified model.
Sources checked on 2026-10-19. This note uses original prompts, reduced metadata, and local checks rather than a verbatim course translation. Confirm API support against the model, endpoint, and SDK version you actually use.