Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~23 min· topic 1 of 7

Topic 7.1

When Text Isn't Enough

In one line

Code that needs a field (an order number, a status) cannot rely on prose. Asking a model for JSON works most of the time and fails in predictable ways: extra words, code fences, cut-off output, and valid JSON of the wrong shape. This topic shows each failure live, and why parsing is not the same as checking.

Think of it like this

A letter versus a form. A friend can write you a lovely letter about your parcel, and you understand it. But the machine that sorts parcels cannot read letters. It needs a form with boxes: *Order number: ___ Status: [ ] shipped [ ] processing*. A friend filling a form may still write in the margin, cross a box twice, or run out of space. Structured output means asking the model to fill the form. Your code's job is to check the boxes before it uses them.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Structured output
A model answer that follows a fixed shape your code can read, usually JSON.
JSON
A text format for data: objects { }, lists [ ], strings, numbers, true, false, null. Strict about quotes and commas.
JSON.parse
The built-in function that turns JSON text into a JavaScript value, or throws a SyntaxError.
Validation
Checking that a parsed value has the shape you expect: the right fields, the right types, allowed values.
Code fence
The three backticks Markdown uses around code. Models often wrap JSON in one, which JSON.parse cannot read.
finishReason
Why the model stopped: stop (finished), length (hit the output limit, so the text is cut off), content-filter, and others.
JSON mode
A provider setting that guarantees syntactically valid JSON, but not your particular shape.
Schema-constrained output
A provider feature where the model can only produce text that fits a JSON Schema you send. Names vary by provider ("structured outputs").
Repair
Code that tries to fix a bad reply, such as cutting out the part between braces. Useful, but never complete.

Step by step

01What you ask for

The weakest form of structure is a sentence in the prompt. The scripted model in the first example answers seven different ways, the way real models sometimes do. Only the first is clean.

the promptwhole filetext
Reply with JSON only, no other words:
{ "orderId": string, "status": "shipped" | "processing" | "cancelled", "days": number }

02Parse it and watch it fail

The first example calls the real generateText of the AI SDK and then runs JSON.parse on text. Read the console: six of seven replies throw, each with its own message. The last-but-one shows finishReason: length, which is the clue that the reply was cut off and no repair can finish it.

In a real app the model would be your provider's, and the one line that changes is model: fakeModel(...), for example model: "provider/model-name" through your provider package.

03Parsed does not mean correct

The second example feeds six replies that all parse, into a function that trusts them. One works. One crashes on a string where a number should be. One prints undefined as the badge text, with no error at all. Three more crash on a missing field, null and [].

The silent one is the dangerous one: nothing threw, and the customer sees the word undefined.

04Repair, and where it stops

The third example is the repair helper most teams write first. It works for words before or after the JSON and for a code fence. It fails when the words contain braces, when there are two objects, and when the reply is cut. And it happily returns "two" for a number.

05The path every structured reply takes

From here on, every structured answer in this course goes through the same four steps, and each step has a failure branch that the screen must show.

The path every structured reply takesdiagram
Rendering diagram…

Try it yourself

  1. 1

    Add your own bad reply

    In the first example, add "with a comment": '{"orderId":"ORD-1042" /* shipped */}' to REPLIES. Run it.

    JSON has no comments, so JSON.parse throws. Models sometimes copy a style from code they have seen.

  2. 2

    Make the silent bug loud

    In the second example, change BADGE[order.status] to BADGE[order.status] ?? "Unknown status". Run it.

    The third line now shows a visible fallback instead of the word undefined. A fallback is better, but it is still a guess about what to do. A schema (next topic) rejects the reply before the card is built.

  3. 3

    Break the repair helper twice

    In the third example, add a reply with a nested object: "nested": '{"order":{"id":1}} thanks'. Then add one with a brace inside a string: "brace in a string": '{"note":"use } carefully"} ok'.

    The first still works because the outer braces match. The second also works by luck. Now try '{"a":1} and {b}': it fails. Hand-made repair depends on luck, and luck is not a plan.

Code & diagrams

Seven replies to "reply with JSON only"

Six of seven replies break JSON.parse. The model here is a scripted stand-in; real models fail like this only some of the time, which is what makes it a trap.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

clean: parsed { orderId: 'ORD-1042', status: 'shipped', days: 2 }
extra words: SyntaxError: Unexpected token 'S', "Sure! Here"... is not valid JSON | finishReason: stop
code fence: SyntaxError: Unexpected token '`', "```json
{""... is not valid JSON | finishReason: stop
single quotes: SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2) | finishReason: stop
trailing comma: SyntaxError: Expected double-quoted property name in JSON at position 50 (line 1 column 51) | finishReason: stop
cut off: SyntaxError: Unterminated string in JSON at position 44 (line 1 column 45) | finishReason: length
empty: SyntaxError: Unexpected end of JSON input | finishReason: stop
Valid JSON, wrong data

Look at the third line: the status was not one of the three allowed values, nothing threw, and the card says "undefined". Crashes are loud. This kind of wrong is quiet.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

rendered: ORD-1042: On the way, arrives in 2 days
crash: {"orderId":"ORD-1042","status":"shipped","days":"two"} -> TypeError: order.days.toFixed is not a function
rendered: ORD-1042: undefined, arrives in 2 days
crash: {"orderId":"ORD-1042","status":"shipped"} -> TypeError: Cannot read properties of undefined (reading 'toFixed')
crash: null -> TypeError: Cannot read properties of null (reading 'orderId')
crash: [] -> TypeError: Cannot read properties of undefined (reading 'toFixed')
A repair helper, and where it stops

Repair rescues the first three. It fails on the next three, and the last one "succeeds" with the wrong type. Use schema-constrained output and validation instead of growing this function.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

extra words: { orderId: 'ORD-1042', days: 2 }
code fence: { orderId: 'ORD-1042', days: 2 }
words after: { orderId: 'ORD-1042', days: 2 }
braces in the words: FAILED Expected property name or '}' in JSON at position 1 (line 1 column 2)
two objects: FAILED Unexpected non-whitespace character after JSON at position 32 (line 1 column 33)
cut off: FAILED no JSON object found
wrong type: { orderId: 'ORD-1042', days: 'two' }

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Trust the parsed object with no check

In the second example, delete the try { and } catch (e) { … } lines so the loop calls console.log("rendered:", describe(data)); directly.

terminal
$ Console
── what you'll see ──
rendered: ORD-1042: On the way, arrives in 2 days
Uncaught TypeError: order.days.toFixed is not a function

Myth vs fact

Myth

If I write "reply with JSON only" in capitals, I will always get JSON.

Fact

A prompt makes JSON more likely, never certain. Even schema-constrained output can be cut off or declined. You still parse and validate every reply.

Myth

If JSON.parse succeeds, the data is safe to use.

Fact

It only proves the text is JSON. The fields can be missing, the wrong type, or values you never allowed.

Myth

A bug that throws is the worst kind.

Fact

A thrown error is easy to find. The reply that parses, passes silently, and shows undefined to a customer is harder to notice.

Myth

Structured output is always better than text.

Fact

Use it when code needs a field. If a person reads the answer, text is better: it costs fewer tokens and does not force the answer into boxes.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Log the raw reply text (not just the parse error) whenever structure fails, along with finishReason. "JSON parse error" alone tells you nothing; the raw text tells you whether it was a fence, a cut-off, or a refusal. Be careful what you log: the text may contain a customer's words (Topic 13.5 covers what to log and what never to log).

  • ▸

    finishReason: "length" is the strongest signal that retrying with the same settings will fail the same way. The fix is a shorter shape or a higher output limit, not another attempt.

  • ▸

    Keep the JSON small. Every key name is paid for in output tokens on every reply, and long answers are cut off more often. Short keys that stay readable (days, not numberOfDaysUntilTheOrderArrivesAtTheCustomer) save money and reduce cut-offs.

Remember this

  1. 1

    The problem this solves: Until now the model's answer was only ever shown to a person. The moment your code must *do* something with the answer (look up an order, pick a badge colour, fill a form field, choose a button), reading prose with string tricks breaks. "It looks like it shipped" is not a value. This topic shows how answers should come back as data, and what goes wrong when they do not.

  2. 2

    Structured output means the model's answer follows a fixed shape that code can read. The usual format is JSON (JavaScript Object Notation): text such as { "orderId": "ORD-1042", "days": 2 }. Names are in double quotes, strings are in double quotes, and there are no comments. JSON.parse(text) turns that text into an object. If the text is not *exactly* valid JSON, it throws a SyntaxError.

  3. 3

    Why "please reply with JSON" breaks. A model writes one token after another, choosing a likely next token. Your instruction makes JSON likely. It does not make it certain. The first live example shows the real failures: a friendly sentence before the JSON, a Markdown code fence (three backticks and the word json) around it, single quotes instead of double, a trailing comma, an answer cut off because it hit the output limit (finishReason: "length"), and an empty reply. Each one makes JSON.parse throw.

  4. 4

    Valid JSON is not valid data. This is the bigger trap. Text can parse perfectly and still be wrong for your screen: days is the string "two", status is "on its way" (not one of your three statuses), a field is missing, or the whole reply is null or []. JSON.parse only answers *"is this JSON?"*. Your code needs a second answer: *"is this the shape my screen expects?"* That second step is called validation, and the next topic does it with zod.

  5. 5

    Repair tricks help a little. A common first fix is to cut the text from the first { to the last }. It rescues the "friendly sentence" and "code fence" cases, and then fails on braces inside the words, on two objects in one reply, and on cut-off output. It cannot fix a wrong type at all. Treat repair as a last resort, not the plan.

  6. 6

    Ask the provider to enforce the shape. Providers offer three levels. (1) Ask in the prompt: weakest. (2) JSON mode: the output is guaranteed to be syntactically valid JSON, but not necessarily your shape. (3) Schema-constrained output (often called structured outputs): the provider only lets the model produce text that fits the JSON Schema you send. Support and the allowed schema features differ by provider and model, so check your provider's documentation. Even at level 3, a reply can still be cut off, a model can decline, and your own code can drift from the schema, so you still validate on your side. Topic 7.3 shows how the AI SDK sends the schema.

  7. 7

    When not to use structure. If a person reads the answer, keep it as text: Mitra's chat replies stay text. Use structure when code needs a *field*: cards, forms, classification ("is this a return or a complaint?"), extraction, choosing which component to show. Structure has a cost: the braces and quotes use output tokens, a half-streamed JSON is harder to show (Topic 7.4), and a strict shape can force the model to squeeze an answer into boxes that do not fit. Give it a way out (Topic 7.3).

Explain it without notes

01

Why is "please return JSON" in a prompt not enough?

02

Name four ways a model's JSON reply can fail before JSON.parse succeeds.

03

A reply parses as JSON. What can still be wrong, and why is that worse than a parse error?

04

What are the three levels of asking a provider for structure, and what does each guarantee?

05

When should an AI feature use text and not structured output?

Practice

01

Write safeParseReply(text) that returns { ok: true, data } or { ok: false, reason } and never throws. It should try JSON.parse, and then check that data is an object with a string orderId and a number days. Test it on three replies of your own.

02

Write a small function describeFailure(finishReason, error) that returns one plain-English sentence for the user: one for a cut-off reply, one for any other unreadable reply. Why should the sentence not mention JSON?

Trade-offs

  • ↔

    Structured output lets code act on the answer, at the cost of a strict shape. The model has less room to explain or hedge, so give it a field for that (Topic 7.3) instead of forcing every answer into the boxes.

  • ↔

    Strong provider-side enforcement reduces failures but ties you to features that differ between providers. Validating on your side is portable and always needed, so build that first.

Done when you can

  • I can show three ways a model's JSON reply breaks JSON.parse.

  • I can explain why a reply that parses can still crash or mislead the screen.

  • I can say what finishReason: "length" means and why retrying does not help.

  • I can describe the three levels of asking for structure and what each one guarantees.

  • I can name the four steps a structured reply takes before it is rendered.