Como fazer o Claude devolver JSON validado na API com saída estruturada (structured outputs)

Para o Claude devolver JSON validado, envie um JSON schema em output_config.format com type "json_schema" na Messages API. No Python, client.messages.parse() aceita um modelo Pydantic e devolve o objeto tipado em parsed_output (no TypeScript, o equivalente usa Zod). O recurso está em GA e não precisa de header beta. Antes de confiar no resultado, cheque o stop_reason, porque refusal e max_tokens podem trazer uma saída fora do schema.
Fala aí, beleza? Se tu tem um código em produção que pega a resposta do Claude e manda direto pra um json.loads(), já sabe o drama: uma vírgula a mais, um texto de introdução antes do objeto, e o pipeline quebra 😛
Aqui o foco é o cenário real de backend: classificar tickets de suporte e extrair campos de um texto, com uma saída que o código seguinte consegue parsear sem gambiarra
Bora ver os dois caminhos nativos da API (output_config.format e strict tool use), como validar, como tratar as falhas e quando pedir JSON no prompt já resolve
O que você precisa antes de pedir JSON estruturado ao Claude
Você precisa de acesso à Messages API e de um JSON schema que descreva o formato da resposta
Se for usar o helper do SDK, troque o schema escrito à mão por um modelo Pydantic (Python) ou Zod (TypeScript), que vai para client.messages.parse()
Se ainda está decidindo entre usar o chat ou integrar via código, vale entender a diferença entre Claude no chat ou pela API antes de seguir, porque tudo aqui é recurso de API
O que é saída estruturada, afinal?
Segundo a documentação de structured outputs, a resposta fica restrita ao schema que você enviou, para que o resultado seja válido e parseável pelo código que vem depois
Formação Claude Code
Domine Claude Code do absoluto zero até o avançado
- 120 aulas
- 4 projetos
- 9h 48min
Hoje o recurso está em GA (disponível para todos) e não precisa mais de header beta 🙂
Tome cuidado com duas incompatibilidades:
- Structured outputs não funciona junto com citations
- No modo de saída JSON, também não combina com prefill de mensagem (aquele truque de começar a resposta do assistente com
{)
Se o teu fluxo depende de um desses dois, vai precisar reorganizar antes
Passo a passo: classificar tickets e extrair campos com JSON schema
A ideia é simples: tu descreve o formato, a API restringe a geração a esse formato e o teu código só precisa conferir se a resposta terminou do jeito certo
Os exemplos abaixo são didáticos: o nome do modelo fica como placeholder de propósito, porque a lista de modelos suportados muda e o lugar certo pra conferir é a página da doc
- Desenhe o schema do ticket só com o que a API suporta
A API aplica diretamente: object, arrays de required, enum, const e validação básica de tipo
Pra classificação de ticket, enum é o melhor amigo: categoria e prioridade viram uma lista fechada e o modelo não inventa uma categoria nova no meio do caminho
{
"type": "object",
"properties": {
"categoria": {
"type": "string",
"enum": ["cobranca", "bug", "acesso", "duvida"]
},
"prioridade": {
"type": "string",
"enum": ["baixa", "media", "alta"]
},
"resumo": {
"type": "string"
}
},
"required": ["categoria", "prioridade", "resumo"]
}
O erro comum deste passo: colocar minimum, maximum, minLength ou pattern (regex) no schema bruto
A API não aplica essas restrições e, se elas aparecem num schema enviado direto, a requisição volta com erro 400
- Envie a requisição com
output_config.format
Na Messages API, o schema vai em output_config.format com type: "json_schema"
O corpo da requisição fica mais ou menos assim (exemplo didático):
{
"model": "SEU_MODELO_COM_SUPORTE",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "Classifique este ticket: Fui cobrado duas vezes no cartao este mes e preciso do estorno"
}
],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"categoria": { "type": "string", "enum": ["cobranca", "bug", "acesso", "duvida"] },
"prioridade": { "type": "string", "enum": ["baixa", "media", "alta"] },
"resumo": { "type": "string" }
},
"required": ["categoria", "prioridade", "resumo"]
}
}
}
}
O resultado esperado é um objeto que respeita o schema: categoria dentro do enum, prioridade dentro do enum e um resumo em texto

VPS Hostinger para seu sistema
10% OFF extraVPS com SSD NVMe e backups semanais. O cupom MATHEUSHORADECODAR é aplicado automaticamente pelo link.
Ver planos com 10% OFF →O erro comum deste passo: seguir tutorial antigo
Quando o recurso saiu, ele estava em beta público e usava o header anthropic-beta: structured-outputs-2025-11-13, com o parâmetro output_format na Messages API
Isso mudou: o header deixou de ser necessário e, na Messages API, output_format foi substituído por output_config.format
- Use
client.messages.parse()para extrair campos já tipados
Aqui entra o caminho mais confortável no Python: tu define um modelo Pydantic, passa ele pro client.messages.parse() e o SDK faz o resto
E o que é esse "resto"? O SDK converte o modelo em schema, envia como output_config.format, valida a resposta e devolve o objeto tipado em parsed_output
Se tu conhece ORM, é bem parecido: tu descreve a classe e para de pensar no formato cru por baixo
Exemplo didático de extração de campos de um ticket:
from typing import Literal
from pydantic import BaseModel, Field
class TicketExtraido(BaseModel):
nome_cliente: str
email: str
categoria: Literal["cobranca", "bug", "acesso", "duvida"]
valor_reclamado: float = Field(ge=0) # restricao que a API nao aplica
# client: cliente do SDK oficial ja configurado
# passe o modelo TicketExtraido para messages.parse conforme a doc
resposta = client.messages.parse(...)
ticket = resposta.parsed_output
print(ticket.categoria, ticket.valor_reclamado)
Repara no Field(ge=0): é um limite numérico, que a API não aplica
Com o helper, isso não quebra a requisição: restrições não suportadas viram texto na description do campo (o modelo recebe como orientação) e o próprio SDK confere na resposta, localmente, dentro do teu código
No TypeScript, o equivalente usa Zod no lugar do Pydantic, beleza? Confira a assinatura exata na doc antes de copiar a lógica do Python
- Cheque o
stop_reasonantes de confiar no JSON
Esse é o passo que mais gente pula, e é onde o pipeline quebra em produção 😀
Dois casos podem trazer saída fora do schema:
stop_reason: "refusal": o Claude recusou o pedido por segurança, e a recusa tem prioridade sobre as restrições do schemastop_reason: "max_tokens": a resposta foi cortada no limite e pode estar incompleta. A orientação da doc é tentar de novo com ummax_tokensmaior
Exemplo didático de tratamento:
def processar_ticket(resposta):
if resposta.stop_reason == "refusal":
# nao parseia: manda para revisao humana ou fila separada
return {"status": "recusado"}
if resposta.stop_reason == "max_tokens":
# saida possivelmente incompleta: refazer com max_tokens maior
return {"status": "refazer"}
return {"status": "ok", "dados": resposta.parsed_output}
O erro comum deste passo: parsear direto, sem olhar o stop_reason
Saída estruturada garante o formato quando a geração termina normalmente, mas não cobre recusa nem corte por limite
- Conte com a latência maior no primeiro uso do schema
Na primeira vez que um schema é usado, a resposta demora mais, porque a gramática é compilada
Depois disso ela fica em cache por 24 horas a partir do último uso
O cache é invalidado quando muda a estrutura do schema ou, se tiver tool use junto, o conjunto de ferramentas
Mudar só name ou description não invalida, então pode ajustar o texto de orientação sem medo

Crie sistemas com IA para vender
Prompts, modelos e checklists para criar e publicar seu sistema com IA e preparar a oferta para o primeiro cliente.
Quero o e-book gratuito Receba o livro e os bônus no seu e-mail.O erro comum deste passo: gerar um schema diferente a cada requisição (montando campos dinâmicos, por exemplo) e pagar a compilação toda vez
- Checagem final
O teste que vale é este: o objeto chega tipado e validado em parsed_output (ou parseável a partir do schema), e o teu código tem um caminho definido para refusal e para max_tokens
Se os dois pontos estão cobertos, o fluxo está pronto pra ligar no pipeline
Strict tool use: quando estruturar a saída por meio de uma ferramenta
O outro caminho nativo é estruturar a saída como o input de uma ferramenta, com strict tool use
Faz sentido quando o teu fluxo já é de agente e ferramentas: em vez de adicionar um formato de resposta separado, tu força o Claude a chamar uma ferramenta cujo input é exatamente o JSON que tu quer
Como funciona?
Segundo a doc de strict tool use, com strict: true como propriedade de nível superior na definição da ferramenta, a geração passa a ser restrita por gramática e só sai input válido segundo o schema
Nesse modo, additionalProperties: false e required são obrigatórios no schema da ferramenta
Exemplo didático do schema de entrada de uma ferramenta de classificação:
{
"type": "object",
"properties": {
"categoria": { "type": "string", "enum": ["cobranca", "bug", "acesso", "duvida"] },
"prioridade": { "type": "string", "enum": ["baixa", "media", "alta"] }
},
"required": ["categoria", "prioridade"],
"additionalProperties": false
}
Os três modos de tool_choice
O tool_choice decide se e qual ferramenta o Claude chama:
auto: o Claude decide se chama alguma ferramentaany: ele precisa usar alguma das ferramentas fornecidastool(comname): força uma ferramenta específica
Combinando tool_choice {"type": "any"} com strict: true, tu garante que alguma ferramenta será chamada e que o input dela segue o schema
Se só existe uma ferramenta de classificação, tool com o name dela deixa a intenção ainda mais explícita
Limites do modo strict
Tem teto de complexidade, se liga:
- No máximo 20 strict tools por requisição
- 24 parâmetros opcionais no total
- 16 parâmetros com union types (
anyOfou arrays de tipo)
Se o teu schema está batendo nesses números, provavelmente vale quebrar em mais de uma etapa
Pra quem orquestra vários agentes conversando entre si, o mesmo raciocínio aparece em saída estruturada entre agentes, onde o parse quebrado de um agente derruba o próximo
E no Agent SDK e nas nuvens?
O Claude Agent SDK tem saída estruturada própria: o JSON Schema vai em outputFormat (TypeScript) ou output_format (Python) do query(), e a mensagem final traz o campo structured_output com os dados validados
Se liga: esse output_format do Agent SDK é outro produto, só tem o mesmo nome por coincidência
Ele não é o output_format antigo do beta da Messages API, que lá foi substituído por output_config.format
Structured outputs para modelos Claude também existe no Amazon Bedrock e no Google Cloud, cada um com documentação própria
Não assuma que os parâmetros são idênticos aos da API da Anthropic: confere na doc de cada plataforma
Quando pedir JSON no prompt já basta (e quando não)
A regra da própria Anthropic é direta: se a conformidade com o JSON schema precisa ser garantida, o caminho é structured outputs, não engenharia de prompt (guia de consistência)
Pedir o formato no prompt e dar exemplos serve para consistência em geral ou quando tu precisa de mais flexibilidade do que um schema estrito oferece
Traduzindo pra decisão prática:
- Use structured outputs quando o código faz parse automático e uma resposta fora do formato quebra alguma coisa (fila de tickets, banco de dados, integração com outro sistema)
- Prompt já basta em protótipo, script de uso pontual ou saída semiestruturada que uma pessoa vai ler de qualquer jeito
- Prompt também ajuda junto com o schema: explicar o critério de cada categoria melhora a escolha dentro do enum, mesmo com o formato garantido pela API
Nada de "prompt mágico" pra forçar JSON perfeito, beleza? Se precisa de garantia, a garantia está no recurso, não na frase
Falando em quanto a instrução guia o modelo, no canal do Hora de Codar tem um vídeo em que as regras vazadas do Claude Code foram coladas num modelo bem mais barato, um contexto legal sobre o peso do prompt
Próximo passo: leve o schema do seu ticket para produção
Escolhe UM fluxo real que hoje depende de parse frágil: a classificação de tickets ou a extração de campos de um formulário
Escreve o schema só com o que a API aplica (object, required, enum, const, tipos) e deixa limites numéricos e regex pro modelo Pydantic validar localmente
Roda via messages.parse, confere o parsed_output com alguns tickets reais e só liga no pipeline depois que refusal e max_tokens têm destino definido no teu código
Qualquer dúvida no caminho, a página de structured outputs da Anthropic é a referência pra conferir modelos suportados e mudanças de parâmetro
até o próximo post! 🙂
Quer aprender mais sobre inteligência artificial e programação na prática? Conheça o canal Hora de Codar no YouTube e acompanhe os próximos vídeos.
Formações
Formação Vibe Coding
Do Prompt ao Produto: Crie Software Real com IA
- 474 aulas
- 20 projetos
- 39h 30min
Blog | Mais populares

Como instalar Claude Code: guia completo para iniciantes
Para instalar Claude Code, use o método indicado para seu sistema e depois execute claude. O instalador nativo dispensa Node.js. O acesso exige uma modalidade […]

Claude Code Preço: quanto custa, planos Pro vs Max e API
Quanto custa o Claude Code? Na assinatura individual, ele está incluído no Pro de US$ 20 por mês (ou US$ 200 cobrados de uma vez […]

Claude Sonnet 5.5 ou Opus 5.5: qual usar em cada tarefa do dia?
Sonnet 5.5 ou Opus 5.5? Veja como a Anthropic divide os papéis, os preços na API e uma regra prática pra escolher em cada tarefa do dia.
