Structured Data

Suppose we are building a web app that generates AWS EventBridge rules. A user types a description of the events they want to capture, clicks Generate, and expects clean JSON they can copy and use immediately.

We wire the button to Claude, deploy, and admire our work. What could possibly go wrong?

The Problem with Default Responses #

Claude, you see, wants to be helpful. Ask it for JSON, and it hands you a small presentation about JSON.

By default, the response looks like this:

```json
{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 Instance State-change Notification"],
  "detail": {
    "state": ["running"]
  }
}
```

This rule captures EC2 instance state changes when instances start running.

The JSON itself is correct. It is also wrapped in a markdown code block and trailed by a friendly sentence of commentary.

So our user clicks Copy, pastes into the AWS console, and gets a parse error. They pasted three backticks, the word json, and a book report.

Claude, our faithful saboteur.

How do we get the raw data and nothing else?

Prefilling + Stop Sequences #

The trick combines two features we haven’t met yet: assistant message prefilling and stop sequences.

One disclosure before we begin. The newest models — the Claude 4.6 family onward, including Sonnet 5 and Opus 5 — have retired assistant prefill entirely. Send claude-sonnet-5 a prefilled assistant message, and you receive a 400: “This model does not support assistant message prefill. The conversation must end with a user message.”

These modern models have moved on to other instruments for controlling output: system prompt instructions, and structured outputs via output_config.format, which constrain the response shape at the API level rather than by sleight of hand.

Why learn prefilling at all, then? Because the technique still runs on earlier models (this lesson uses claude-haiku-4-5) and because it is a useful feather to have in your hat. It teaches you exactly how output gets shaped, which is knowledge the fancier instruments quietly depend on.

Now, behold:

const messages: Anthropic.MessageParam[] = [];

addUserMessage(messages, "Generate a very short event bridge rule as json");
addAssistantMessage(messages, "```json");

const text = await chat(messages, { stopSequences: ["```"] });

Two changes from our usual routine. Let us take them one at a time.

First, the prefill. We append an assistant message containing just `json to the opening of a markdown code block. When Claude receives the conversation, it believes it has already begun answering, and that its answer so far consists of an opened code block. All it can do is continue: it writes the JSON content, and only the JSON content.

Second, the stop sequence. Eventually Claude finishes the JSON and reaches for the closing ` — the moment where, left unsupervised, it would seal the block and launch into commentary. The stop sequence ` tells the API: the instant this string appears, cease generation. The gate slams shut mid-backtick.

The result:

{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 Instance State-change Notification"],
  "detail": {
    "state": ["running"]
  }
}

No fences. No commentary.

Processing the Response #

The response is not pristine. You may notice some stray newline characters around the edges. Harmless, and easily dealt with:

// Trim the stray whitespace, then parse
const rule = JSON.parse(text.trim());

trim() removes the loose newlines, JSON.parse confirms the payload is genuine JSON, and we are left holding an actual object rather than a string impersonating one.

Is this technique only for JSON? #

It works anytime we need structured data without commentary: Python code snippets, bulleted lists, CSV data, or any formatted content where we want the content itself, not a lecture about it.

More importantly, the recipe generalises. Identify what Claude naturally wants to wrap your content in, then use the opening wrapper as your prefill and the closing wrapper as your stop sequence. For code, that is usually a markdown code block; for lists, it may be different formatting markers.

Couldn’t We Just Use Zod? #

Simply parse whatever Claude returns with a tool like Zod, and be done with it:

const EventBridgeRule = z.object({
  source: z.array(z.string()),
  "detail-type": z.array(z.string()),
});

const rule = EventBridgeRule.parse(JSON.parse(text));

But parse it into what, exactly?

Zod is a validator. Hand it clean JSON, and it will confirm the shape magnificently. Hand it three backticks and a book report, and it will fail.

Failing loudly is great. But it answers the wrong question. Zod tells us that the output is garbage. Our problem is figuring out what to do when we have to deal with garbage.

This is what prefilling and stop sequences do: they shape the generation itself. The two work together: you shape the output with prefill and stop sequences, then parse the result with Zod. Now a validation failure means something went wrong, not something predictable that went unhandled.

Exercise #

Use message prefilling and stop sequences to get three different sample AWS CLI commands in a single response. Each command should be short, and there must be no comments or explanation anywhere in the output.

A hint: message prefilling is not limited to characters like `. An assistant message can begin with anything.

If you get stuck, the exercise walkthrough on Anthropic Academy shows a solution in Python.

Please give this a shot before reading the solution below.

The Solution #

The hint is doing heavy lifting. A prefill can be an entire sentence, one that commits Claude to a course of action as surely as an opened code block does:

const messages: Anthropic.MessageParam[] = [];

addUserMessage(
  messages,
  "Generate three different sample AWS CLI commands. Each should be very short.",
);
addAssistantMessage(
  messages,
  "Here are three commands in a single code block without any comments:\n```bash",
);

const text = await chat(messages, { stopSequences: ["```"] });

The prefilled sentence declares that all three commands are coming in one code block, with no comments. Claude, believing it already said so, obliges. The opened `bash fence puts it in command-writing mode, and the stop sequence cuts generation the instant it tries to close the fence:

aws s3 ls
aws ec2 describe-instances --region us-east-1
aws iam list-users

Three commands, one response, not a word of commentary. The prefill is Claude’s own voice, turned into an instrument of precision.

Repo #

Code exercises set up here

 
0
Kudos
 
0
Kudos

Now read this

Response Streaming

Suppose we have built a chat application. A user types a question, presses Enter, and our server dutifully forwards it to Claude. Claude begins composing a response. The process can take ten, twenty, thirty seconds. And what does our... Continue →