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

Ilustração representando a API do Claude retornando dados em formato JSON estruturado
Resposta rápida

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

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

  1. 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

  1. 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

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

  1. 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

  1. Cheque o stop_reason antes 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 schema
  • stop_reason: "max_tokens": a resposta foi cortada no limite e pode estar incompleta. A orientação da doc é tentar de novo com um max_tokens maior

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

  1. 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

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

  1. 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 ferramenta
  • any: ele precisa usar alguma das ferramentas fornecidas
  • tool (com name): 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 (anyOf ou 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

Vídeo complementar do canal Hora de Codar, sobre um assunto relacionado.

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.

Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Formações

Formação Vibe Coding

Formação Vibe Coding

Do Prompt ao Produto: Crie Software Real com IA

  • 474 aulas
  • 20 projetos
  • 39h 30min

Blog | Mais populares