Respostas em JSON na API do Claude: saídas estruturadas com esquema
Pedir "responda em JSON" no prompt funciona quase sempre, e o "quase" é o problema: um texto antes do JSON, um campo com outro nome, uma vírgula a mais, e o seu código quebra.
As saídas estruturadas (structured outputs) resolvem isso. Você passa um JSON Schema, e a resposta do Claude Opus 5 segue o esquema: os campos certos, com os tipos certos.
Este guia mostra três jeitos de usar: com Pydantic em Python, com Zod em TypeScript e com o JSON Schema direto, que serve para qualquer linguagem.
Quando usar
- Extrair dados de texto livre: nome, e-mail, número do pedido e datas de uma mensagem de cliente.
- Classificar: categoria, urgência ou assunto de um chamado.
- Gerar dados para o seu código: listas, formulários, configurações.
Se você quer que o Claude chame uma função do seu código, o recurso certo é o tool use.
Python com Pydantic
O jeito mais simples em Python: descreva o formato numa classe do Pydantic e use o messages.parse.
from typing import Literal
import anthropic
from pydantic import BaseModel, Field
class Chamado(BaseModel):
cliente: str
email: str
pedido: str | None
categoria: Literal["pagamento", "entrega", "troca", "outro"]
urgente: bool
resumo: str = Field(description="O problema em uma frase")
client = anthropic.Anthropic() # ANTHROPIC_BASE_URL=https://api.open33.tech
mensagem = """Oi, aqui é a Ana Souza (ana@exemplo.com.br). Paguei o pedido 10231
no Pix faz 3 dias e ainda aparece como pendente. Preciso dele até sexta!"""
resposta = client.messages.parse(
model="claude-opus-5",
max_tokens=16000,
system="Extraia os dados do chamado de suporte.",
messages=[{"role": "user", "content": mensagem}],
output_format=Chamado,
)
chamado = resposta.parsed_output # um objeto Chamado, já validado
print(chamado.categoria, chamado.urgente)
print(chamado.model_dump_json(indent=2))
O que acontece:
- O SDK gera o JSON Schema a partir da classe, manda junto com o pedido e valida a resposta. O
parsed_outputjá é um objetoChamado. - O
Literalvira uma lista fechada de valores: o Claude só pode escolher uma das categorias. - O
str | Nonedeixa o campo vir comonullquando a mensagem não traz a informação, em vez de o modelo inventar um valor. - A
descriptionexplica o campo para o modelo. Use nos campos em que o nome sozinho não basta.
TypeScript com Zod
Em TypeScript, o esquema vem do Zod, e o tipo da resposta sai dele:
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const Chamado = z.object({
cliente: z.string(),
email: z.string(),
pedido: z.string().nullable(),
categoria: z.enum(["pagamento", "entrega", "troca", "outro"]),
urgente: z.boolean(),
resumo: z.string().describe("O problema em uma frase"),
});
const client = new Anthropic(); // ANTHROPIC_BASE_URL=https://api.open33.tech
const mensagem =
"Oi, aqui é a Ana Souza (ana@exemplo.com.br). Paguei o pedido 10231 no Pix " +
"faz 3 dias e ainda aparece como pendente. Preciso dele até sexta!";
const resposta = await client.messages.parse({
model: "claude-opus-5",
max_tokens: 16000,
system: "Extraia os dados do chamado de suporte.",
messages: [{ role: "user", content: mensagem }],
output_config: { format: zodOutputFormat(Chamado) },
});
const chamado = resposta.parsed_output;
if (!chamado) throw new Error("A resposta veio sem texto");
console.log(chamado.categoria, chamado.urgente);
O chamado tem o tipo do esquema: o editor completa os campos, e o TypeScript avisa se você usar um campo que não existe. O Zod também confere a resposta quando ela chega: se algum campo vier fora do esquema, o messages.parse lança um erro.
JSON Schema direto, em qualquer linguagem
Sem Pydantic nem Zod, passe o esquema em output_config.format:
import json
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_BASE_URL=https://api.open33.tech
esquema = {
"type": "object",
"properties": {
"cliente": {"type": "string"},
"itens": {
"type": "array",
"items": {
"type": "object",
"properties": {
"produto": {"type": "string"},
"quantidade": {"type": "integer"},
},
"required": ["produto", "quantidade"],
"additionalProperties": False,
},
},
"entrega": {"type": "string", "enum": ["retirada", "correios", "transportadora"]},
},
"required": ["cliente", "itens", "entrega"],
"additionalProperties": False,
}
resposta = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": "Pedido da Marina: 2 camisetas pretas e 1 boné. Ela vai retirar na loja.",
}],
output_config={"format": {"type": "json_schema", "schema": esquema}},
)
if resposta.stop_reason != "end_turn":
raise RuntimeError(f"Resposta incompleta: {resposta.stop_reason}")
texto = next(b.text for b in resposta.content if b.type == "text")
pedido = json.loads(texto)
print(pedido["itens"])
O JSON vem no bloco de texto da resposta. Como em qualquer chamada ao Claude Opus 5, procure o bloco de type == "text": pode vir um bloco de raciocínio antes dele.
O mesmo pedido com curl, que serve de modelo para qualquer linguagem:
curl -sS https://api.open33.tech/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 16000,
"messages": [{"role": "user", "content": "A Ana quer trocar o tamanho do tênis do pedido 10231."}],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"pedido": {"type": "string"},
"categoria": {"type": "string", "enum": ["pagamento", "entrega", "troca", "outro"]}
},
"required": ["pedido", "categoria"],
"additionalProperties": false
}
}
}
}' | jq -r '.content[] | select(.type == "text") | .text'
O jq no fim mostra só o JSON da resposta.
Regras do esquema
A API aceita quase todo o JSON Schema, com algumas regras:
- Todo objeto precisa de
"additionalProperties": false. - Aceitos: os tipos
object,array,string,integer,number,booleanenull;enum,const,anyOf,allOf,$refe$defs; e formatos de texto comodate,date-time,email,urieuuid. - Não aceitos: esquemas recursivos, limites de número (
minimum,maximum) e limites de tamanho de texto (minLength,maxLength).
Com Pydantic ou Zod, o SDK cuida disso: acrescenta o additionalProperties, tira do esquema as regras que a API não aceita, como um Field(ge=1), e confere essas regras quando a resposta chega.
A primeira requisição com um esquema novo demora um pouco mais, porque a API prepara o esquema. Depois, ele fica guardado por 24 horas, e as requisições seguintes com o mesmo esquema não têm essa espera.
Confira se a resposta veio completa
O esquema vale quando a resposta termina normalmente, com stop_reason igual a "end_turn". Há dois casos em que não:
"max_tokens": a resposta foi cortada. Aumente omax_tokens. No Claude Opus 5, ele limita o raciocínio e a resposta juntos."refusal": o Claude recusou o pedido, e a resposta pode não seguir o esquema.
Com o messages.parse, uma resposta fora do esquema vira erro: pydantic.ValidationError em Python e AnthropicError em TypeScript. Trate o erro:
import pydantic
try:
resposta = client.messages.parse(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": mensagem}],
output_format=Chamado,
)
except pydantic.ValidationError as erro:
print("A resposta não seguiu o esquema:", erro)
else:
chamado = resposta.parsed_output
JSON nos argumentos das ferramentas: strict
No tool use, o strict: true garante que os argumentos que o Claude manda para a ferramenta sigam o input_schema, com as mesmas regras de esquema:
ferramentas = [{
"name": "agendar_entrega",
"description": "Agenda a entrega de um pedido numa data e num período.",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"pedido": {"type": "string"},
"data": {"type": "string", "format": "date"},
"periodo": {"type": "string", "enum": ["manha", "tarde"]},
},
"required": ["pedido", "data", "periodo"],
"additionalProperties": False,
},
}]
Os dois recursos se completam: o strict cuida dos argumentos das ferramentas, e o output_config cuida da resposta final. Dá para usar os dois na mesma requisição.
Por que não pedir JSON só no prompt
Sem as saídas estruturadas, dá para pedir JSON no prompt e torcer. Costuma funcionar, mas nada impede um texto antes do JSON, um campo faltando ou um nome trocado. O truque antigo de começar a resposta do assistente com { também não serve mais: o Claude Opus 5 recusa esse preenchimento com erro 400.
Use as saídas estruturadas sempre que outro programa for ler a resposta. Se você está vindo da OpenAI, o guia Migrar da OpenAI para o Claude mostra como trocar o response_format.
Perguntas frequentes
A API do Claude tem modo JSON?
Tem. Com as saídas estruturadas, você passa um JSON Schema em output_config.format, e a resposta segue o esquema. É mais seguro que um modo JSON simples, porque garante os campos e os tipos, e não só um JSON válido.
Qual a diferença entre output_config e strict?
O output_config.format define o formato da resposta final. O strict: true vale para as ferramentas: garante que os argumentos sigam o input_schema. Dá para usar os dois juntos.
Por que a API recusou o meu esquema?
As causas mais comuns são um objeto sem "additionalProperties": false, um esquema recursivo ou regras não aceitas, como minimum e maxLength. Com Pydantic ou Zod, o SDK ajusta o esquema para você.
Funciona com streaming?
Funciona. Em Python, use client.messages.stream(..., output_format=Chamado) e, no fim, pegue o resultado com stream.get_final_message().parsed_output.
Preciso descrever o JSON no prompt também?
Não: o esquema já vai no pedido. Mas explicar o que cada campo significa melhora o conteúdo. Use Field(description=...) no Pydantic ou .describe() no Zod.