Introduction
Hi!!
New week, new post :D
Last week we talked about the basic concepts of an LLM.
Today I want to talk about how to get structured outputs.
Very useful when you need to work with an LLM and use its replies not just as text, but with a specific format so you can process that answer :D
Let's go
Topics
- Few-shot prompting
- Structured outputs
- Difference between them
- Examples with Anthropic and OpenAI
- Anthropic
- OpenAI
- Differences
Let's start with the first one
Few-shot prompting
It's when we teach the model how we want to receive the answer through examples.
For example, if we want to extract data from job posts, the prompt could be something like this:
Extract the job title and if it's remote. Answer in JSON. Example 1: Post: "We are looking for a Junior Frontend Developer. Office in Madrid." Answer: {"title": "Junior Frontend Developer", "remote": false} Example 2: Post: "Senior Data Engineer, 100% remote." Answer: {"title": "Senior Data Engineer", "remote": true} Now this one: Post: "We're hiring a Backend Engineer to work from anywhere." Answer: The model sees the pattern and follows it. But it's only a suggestion: most of the time it works, but sometimes the model adds text before the JSON, forgets a field or uses another name for it. And if your code is waiting for a JSON, that breaks everything.
Structured outputs
We control the format of the answer with precision.
Instead of examples, we send the API a JSON Schema with the exact format that we want, and the API guarantees that the answer follows it.
Few shot-prompting vs Structuted outputs
The difference between them is that in the first one you use examples of how you want to receive the answer, and in the second one you pass the exact schema that you need and the model has to follow it.
| Few-shot prompting | Structured outputs | |
|---|---|---|
| How | Examples in the prompt | JSON Schema in the request |
| Format guaranteed? | No | Yes |
| Works with any model? | Yes | Only models that support it |
They're not enemies: you can use both together. The schema controls the format, and the examples help with the content (for example, how to decide if a job is "senior" or "mid").
Examples with Anthropic and OpenAI
Both APIs support structured outputs now, and both TypeScript SDKs let you use a Zod schema instead of writing the JSON Schema by hand. The SDK converts it to JSON Schema, sends it, and gives you back the answer already parsed and typed.
We will use the same schema for both:
import { z } from "zod"; const JobPosting = z.object({ title: z.string(), company: z.string(), remote: z.boolean(), seniority: z.enum(["junior", "mid", "senior"]), skills: z.array(z.string()), }); const jobText = ` We're hiring a Senior Backend Engineer at Acme to join our team. Fully remote. You'll work with Node.js, TypeScript and PostgreSQL. `; Anthropic
import Anthropic from "@anthropic-ai/sdk"; import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; const client = new Anthropic(); const response = await client.messages.parse({ model: "claude-sonnet-5-5", max_tokens: 1024, messages: [{ role: "user", content: `Extract the data from this job posting:\n${jobText}` }], output_config: { format: zodOutputFormat(JobPosting), }, }); console.log(response.parsed_output?.title); // "Senior Backend Engineer" The response looks like this (simplified):
{ "id": "msg_01...", "type": "message", "role": "assistant", "model": "claude-sonnet-5-5", "content": [ { "type": "text", "text": "{\"title\":\"Senior Backend Engineer\",\"company\":\"Acme\",\"remote\":true,\"seniority\":\"senior\",\"skills\":[\"Node.js\",\"TypeScript\",\"PostgreSQL\"]}" } ], "stop_reason": "end_turn", "usage": { "input_tokens": 210, "output_tokens": 45 }, "parsed_output": { "title": "Senior Backend Engineer", "company": "Acme", "remote": true, "seniority": "senior", "skills": ["Node.js", "TypeScript", "PostgreSQL"] } } -
content: a list of blocks. The JSON comes as a string inside atextblock. -
parsed_output: this one doesn't come from the API, the SDK adds it. It's the JSON already parsed and validated with your Zod schema, so it has the right types in TypeScript. -
stop_reason: why the model stopped.end_turnmeans it finished OK. If it'smax_tokens, the JSON can be cut in the middle. If it'srefusal, the model refused to answer and the output may not match the schema. -
usage: the tokens you sent and received (what you pay for).
OpenAI
import OpenAI from "openai"; import { zodTextFormat } from "openai/helpers/zod"; const client = new OpenAI(); const response = await client.responses.parse({ model: "gpt-6-astra", input: [{ role: "user", content: `Extract the data from this job posting:\n${jobText}` }], text: { format: zodTextFormat(JobPosting, "job_posting"), }, }); console.log(response.output_parsed?.title); // "Senior Backend Engineer" The response looks like this (simplified):
{ "id": "resp_...", "object": "response", "status": "completed", "model": "gpt-6-astra", "output": [ { "type": "message", "role": "assistant", "content": [ { "type": "output_text", "text": "{\"title\":\"Senior Backend Engineer\",\"company\":\"Acme\",\"remote\":true,\"seniority\":\"senior\",\"skills\":[\"Node.js\",\"TypeScript\",\"PostgreSQL\"]}" } ] } ], "usage": { "input_tokens": 205, "output_tokens": 48, "total_tokens": 253 }, "output_parsed": { "title": "Senior Backend Engineer", "company": "Acme", "remote": true, "seniority": "senior", "skills": ["Node.js", "TypeScript", "PostgreSQL"] } } -
output: a list of items. The answer is inside themessageitem, in anoutput_textblock. With reasoning models there can be other items before it, so don't just takeoutput[0]. -
output_parsed: same asparsed_outputin Anthropic, the SDK adds it with the JSON already parsed and typed. -
status:completedif everything went OK,incompleteif it was cut (for example, because of the token limit). -
usage: same as Anthropic, plustotal_tokens. - If the model refuses, instead of
output_textyou get a block withtype: "refusal".
Differences
The idea is the same, what changes is the name of things:
| Anthropic | OpenAI | |
|---|---|---|
| Method | messages.parse() | responses.parse() |
| Where the schema goes | output_config.format | text.format |
| Zod helper | zodOutputFormat(schema) | zodTextFormat(schema, "name") |
| Parsed answer | parsed_output | output_parsed |
| How to know if it finished OK | stop_reason | status |
Wrapping up
I know that the topic of today was short.
But it will be useful for the next few weeks, when we start to talk about n8n and automations.
See you next week :D


