Introduction

Hi!!
New week, new post :D

Reptile saying hi

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

  1. Few-shot prompting
  2. Structured outputs
  3. Difference between them
  4. 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 a text block.
  • 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_turn means it finished OK. If it's max_tokens, the JSON can be cut in the middle. If it's refusal, 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 the message item, in an output_text block. With reasoning models there can be other items before it, so don't just take output[0].
  • output_parsed: same as parsed_output in Anthropic, the SDK adds it with the JSON already parsed and typed.
  • status: completed if everything went OK, incomplete if it was cut (for example, because of the token limit).
  • usage: same as Anthropic, plus total_tokens.
  • If the model refuses, instead of output_text you get a block with type: "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

Sponge Bob saying bye