Structured Outputs em LLMs: JSON Schema com OpenAI, Anthropic e Gemini (2026)

Guia prático de structured outputs em LLMs com JSON Schema: implementação em OpenAI, Anthropic e Gemini, unificação via Instructor e Pydantic-AI, armadilhas de produção e como validar respostas com segurança.

Structured Outputs LLM: JSON Schema Guia 2026

Atualizado: 13 de julho de 2026

Structured Outputs é o recurso que força um LLM a retornar JSON estritamente conforme um schema declarado, eliminando parsers frágeis e loops de retry por saída malformada. Em 2026, OpenAI, Anthropic e Google Gemini oferecem o recurso com APIs distintas: a OpenAI usa response_format com json_schema, a Anthropic reaproveita tool use com input_schema, e a Gemini expõe responseSchema nativo. Este guia mostra como implementar cada uma em produção, quando cada abordagem quebra, e como o Instructor e o Pydantic-AI unificam as três atrás de um único tipo Python.

  • Os três grandes provedores agora suportam structured outputs, mas com contratos incompatíveis. A OpenAI garante conformidade 100% via decodificação restrita; Anthropic e Gemini fazem best-effort em modelos mais antigos.
  • A OpenAI exige additionalProperties: false e required em todas as propriedades. Omitir isso é a causa #1 de erro 400 silencioso em produção (eu mesmo já perdi um sábado inteiro por causa disso).
  • Structured outputs adicionam entre 200 ms e 800 ms de latência na primeira chamada por schema (compilação do FSM), depois cacheiam. Batch API neutraliza esse custo.
  • Function calling e structured outputs se sobrepõem. Se você só precisa de um objeto de resposta, prefira response_format; use tools quando o modelo precisar escolher entre múltiplas ações.
  • Instructor (Python/JS) e Pydantic-AI abstraem as três APIs com validação, retries automáticos e streaming parcial. É a base do que eu uso em produção hoje.
  • Gemini 2.5 Pro aceita propertyOrdering para influenciar a ordem de geração, o que reduz alucinações em campos dependentes.

O que são structured outputs em LLMs

Structured outputs é a garantia, implementada via decodificação restrita (ou constrained sampling), de que a resposta do modelo casa exatamente com um JSON Schema fornecido pelo cliente. A diferença em relação ao antigo json_mode é substantiva: json_mode só garantia que a saída era JSON válido, não que os campos, tipos ou enums corretos apareceriam. Com structured outputs, o servidor mascara probabilisticamente os tokens que violariam o schema durante o sampling, então saídas inválidas se tornam impossíveis, não apenas raras.

Na prática, isso substitui três padrões que todo mundo já implementou pelo menos uma vez. Um, regex para extrair JSON de respostas Markdown. Dois, loops while retry < N chamando o modelo de novo quando o parser falha. Três, prompts defensivos gigantes tipo "responda APENAS com JSON válido, sem texto adicional". Passei quatro anos escrevendo conectores para bancos e seguradoras, sistemas onde uma resposta malformada em produção significa investigação de auditoria. Essa camada de garantia muda o cálculo de risco de agentes LLM em domínios regulados.

Nem todo modelo suporta o recurso. Em julho de 2026, a matriz é assim: OpenAI (todos os GPT-4o+ e GPT-5), Anthropic (Claude 3.5 Sonnet em diante via tool use, Claude 4 com output_format dedicado), Google (Gemini 1.5+ e 2.5 Pro/Flash). Modelos open-source dependem de backends como vLLM com outlines ou llama.cpp com grammar. Vale conferir a especificação do JSON Schema Draft 2020-12 antes de assumir compatibilidade total.

OpenAI: response_format com json_schema

A OpenAI implementa structured outputs via response_format={"type": "json_schema", ...} na Chat Completions e Responses APIs. É a implementação mais estrita das três: o schema é compilado em um autômato finito determinístico na primeira chamada, e o serviço rejeita qualquer schema que não siga o subset suportado. Veja a documentação oficial de Structured Outputs da OpenAI para o subset completo.

Exemplo mínimo em Python usando Pydantic (a integração canônica):

from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Literal

client = OpenAI()

class TicketTriage(BaseModel):
    prioridade: Literal["baixa", "media", "alta", "critica"]
    categoria: Literal["billing", "bug", "feature", "onboarding"]
    resumo: str = Field(description="Ate 140 caracteres")
    requer_humano: bool

resp = client.chat.completions.parse(
    model="gpt-5",
    messages=[
        {"role": "system", "content": "Classifique tickets de suporte."},
        {"role": "user", "content": "Fatura duplicada em maio, cliente pediu estorno."},
    ],
    response_format=TicketTriage,
)

ticket = resp.choices[0].message.parsed
print(ticket.prioridade, ticket.categoria)

Três armadilhas que já me custaram noites:

  • Todo objeto precisa de additionalProperties: false. Pydantic gera isso automaticamente, mas se você monta o schema à mão, esquece uma vez e a API retorna 400 com uma mensagem genérica que não aponta o campo culpado.
  • Todos os campos são required. A OpenAI não aceita campos opcionais no sentido tradicional. Para expressar opcionalidade, use Optional[X] (que vira anyOf: [X, "null"]).
  • Máximo de 100 propriedades e 5 níveis de aninhamento. Schemas profundos precisam ser divididos.

A OpenAI cacheia o autômato compilado por schema. A primeira chamada com um schema novo custa até 800 ms extras; as seguintes voltam à latência normal. Se o seu workload varia muito de schema, isso vira um problema.

Anthropic: tool use como structured output

Historicamente, a Anthropic não expôs um parâmetro dedicado equivalente ao response_format da OpenAI. O padrão idiomático foi declarar uma tool com o schema desejado e forçar o modelo a chamá-la via tool_choice. Em Claude 4 (lançado no início de 2026), há um output_format nativo, mas o padrão via tool ainda funciona e é o que roda na maioria das bases de código existentes. Se você precisa de detalhes sobre a mecânica de tool use, cobri isso no meu guia sobre function calling e tool use em agentes de IA.

import anthropic

client = anthropic.Anthropic()

schema = {
    "type": "object",
    "properties": {
        "prioridade": {"type": "string", "enum": ["baixa", "media", "alta", "critica"]},
        "categoria": {"type": "string", "enum": ["billing", "bug", "feature", "onboarding"]},
        "resumo": {"type": "string", "maxLength": 140},
        "requer_humano": {"type": "boolean"},
    },
    "required": ["prioridade", "categoria", "resumo", "requer_humano"],
}

msg = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=[{
        "name": "triagem_ticket",
        "description": "Registra a classificacao de um ticket.",
        "input_schema": schema,
    }],
    tool_choice={"type": "tool", "name": "triagem_ticket"},
    messages=[{"role": "user", "content": "Fatura duplicada em maio, cliente pediu estorno."}],
)

# A saida estruturada vem em msg.content[0].input
ticket = msg.content[0].input

Duas diferenças importantes em relação à OpenAI. Primeiro, a Anthropic não aplica constrained decoding tão estrita. Em modelos mais antigos (Claude 3 Haiku, por exemplo), você ainda pode ver saídas ocasionais que violam maxLength ou enum. Sempre valide no cliente. Segundo, o campo description em cada propriedade do schema tem influência real no output. A Anthropic usa isso quase como um sub-prompt, então escreva descrições claras e não deixe vazio. A documentação oficial de tool use da Anthropic lista as versões de modelo que suportam o modo estrito.

A vantagem do padrão tool-use é que ele compõe naturalmente com agentes multi-tool. Se o modelo tem cinco ferramentas disponíveis e uma delas é "registrar resposta estruturada final", você ganha estruturação sem duplicar a integração de tool calling.

Google Gemini: responseSchema nativo

O Gemini usa a abordagem mais direta das três: você passa responseMimeType: "application/json" e responseSchema na configuração de geração, e o modelo retorna JSON conforme. A documentação oficial de structured output do Gemini descreve o subset suportado, que inclui a maior parte do OpenAPI 3.0 schema (não o JSON Schema Draft 2020-12 completo).

from google import genai
from google.genai import types
from pydantic import BaseModel
from typing import Literal

client = genai.Client()

class TicketTriage(BaseModel):
    prioridade: Literal["baixa", "media", "alta", "critica"]
    categoria: Literal["billing", "bug", "feature", "onboarding"]
    resumo: str
    requer_humano: bool

resp = client.models.generate_content(
    model="gemini-2.5-pro",
    contents="Fatura duplicada em maio, cliente pediu estorno.",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=TicketTriage,
    ),
)

ticket = TicketTriage.model_validate_json(resp.text)

Uma feature exclusiva do Gemini vale destacar: propertyOrdering. O Gemini gera campos na ordem em que aparecem no schema (não na ordem alfabética que muitas bibliotecas usam). Isso importa porque LLMs são autoregressivos. Se você quer que o modelo "pense" antes de decidir, coloque campos de raciocínio antes de campos de veredicto:

# Campo 'analise' vem antes de 'prioridade': o modelo raciocina primeiro
schema = types.Schema(
    type="OBJECT",
    property_ordering=["analise", "prioridade", "categoria"],
    properties={
        "analise": types.Schema(type="STRING"),
        "prioridade": types.Schema(type="STRING", enum=["baixa", "media", "alta"]),
        "categoria": types.Schema(type="STRING"),
    },
    required=["analise", "prioridade", "categoria"],
)

Testei essa técnica em um triador de bugs de infraestrutura: adicionar um campo reasoning de aproximadamente 80 tokens antes do severity reduziu falsos "critical" em cerca de 22% em uma amostra de 500 chamados. Honestamente, foi a melhoria de menor esforço e maior impacto que já apliquei em uma pipeline de classificação.

Function calling vs structured outputs: qual usar?

A confusão é legítima porque OpenAI e Anthropic historicamente tratam os dois como o mesmo mecanismo. A regra prática que eu uso está na tabela abaixo.

DimensãoFunction calling / toolsStructured outputs
Objetivo primárioModelo escolhe qual ação executarModelo formata a resposta final
Número de "opções"Múltiplas ferramentas disponíveisUm único schema fixo
IteraçãoLoop agente, tool, agenteUma passagem, resposta terminal
Overhead de tokensDefinição de todas as tools no systemUm schema, geralmente menor
Latência típicaN chamadas para N toolsUma única chamada
Melhor paraAgentes, RAG com re-rankingExtração, classificação, ETL
Validação servidorDepende do provedorEstrita na OpenAI, best-effort no resto

Instructor e Pydantic-AI: unificando os três provedores

Em produção, você quase nunca quer amarrar seu código a um único provedor. Falhas regionais, custo variável e melhorias de modelo tornam a portabilidade valiosa. Duas bibliotecas Python resolvem isso hoje: Instructor (mais antiga, mais leve) e Pydantic-AI (framework completo do time do Pydantic).

Exemplo com Instructor abstraindo os três provedores por trás do mesmo BaseModel:

import instructor
from pydantic import BaseModel
from anthropic import Anthropic
from openai import OpenAI

class Ticket(BaseModel):
    prioridade: str
    resumo: str

# OpenAI
oi = instructor.from_openai(OpenAI())
t1 = oi.chat.completions.create(
    model="gpt-5",
    response_model=Ticket,
    messages=[{"role": "user", "content": "Fatura duplicada"}],
    max_retries=3,
)

# Anthropic, mesma API cliente
ai = instructor.from_anthropic(Anthropic())
t2 = ai.messages.create(
    model="claude-sonnet-4-5",
    response_model=Ticket,
    max_tokens=1024,
    messages=[{"role": "user", "content": "Fatura duplicada"}],
    max_retries=3,
)

# Gemini
import google.generativeai as genai
gi = instructor.from_gemini(genai.GenerativeModel("gemini-2.5-pro"))

O Instructor cuida de três coisas que eu antes escrevia à mão em todo projeto: validação Pydantic da resposta, retry automático com feedback do erro no próximo prompt (o modelo vê a exceção e corrige), e streaming de campos parciais (útil quando você quer renderizar um objeto grande incrementalmente na UI). Pydantic-AI vai além e adiciona um agent loop completo com dependências injetáveis. É bom se você está começando do zero, exagero se você só precisa de saída estruturada.

Validação, retries e falhas silenciosas

Mesmo com structured outputs estritos, você precisa validar no cliente. Três razões:

  1. Semântica além do schema. Um campo data_pagamento tipo string com formato ISO 8601 passa no schema, mas o modelo pode entregar "2026-13-45". É sintaticamente válido, semanticamente lixo. Pydantic com datetime pega isso; JSON Schema puro não.
  2. Regras de negócio inter-campo. Se categoria == "billing" exige valor_disputa > 0, isso é um validador cross-field. Um Pydantic @model_validator é o lugar certo. Nem OpenAI nem Anthropic expõem esse tipo de restrição no schema.
  3. Fallback para modelos antigos. Se a sua rota de fallback usa Claude 3 Haiku ou Gemini Flash, a garantia não é estrita.

Meu padrão de retry em produção usa exponential backoff diferenciado. Erro de schema (400 da OpenAI) faz retry com o texto do erro adicionado ao prompt, não backoff cego; erro 429 rate-limit dispara backoff exponencial; erro 500 do provedor aciona um circuit breaker que rota para provedor alternativo. Se você quer profundidade em como estruturar essa camada de resiliência, escrevi antes sobre observabilidade de LLMs em produção. As métricas certas fazem a diferença entre debugar em minutos ou em horas.

Latência, custo de tokens e streaming

Três impactos mensuráveis de structured outputs no seu SLA:

Latência de primeira chamada por schema. A OpenAI compila o schema em FSM na primeira chamada. Mediu-se entre 200 ms e 800 ms para schemas de 20 a 50 propriedades. Chamadas subsequentes com o mesmo schema (hash) reusam o cache. Se você tem 500 schemas diferentes rotando por dia, isso vira custo real; se você tem 5 schemas estáveis, é imperceptível.

Tokens de saída. JSON estruturado inclui aspas, chaves, dois-pontos: tipicamente 20 a 35% de overhead vs texto plano. Para extração de campos curtos, isso pode dobrar sua conta de output tokens. Batch API (OpenAI e Anthropic) corta 50% desse custo se você aceita janela de resposta de até 24 horas, perfeito para pipelines de ETL noturnos.

Streaming parcial. Você pode fazer streaming de tokens dentro de structured outputs. A OpenAI expõe stream=True e cada delta contém tokens JSON parciais que você acumula. Instructor tem Partial[MyModel] que retorna instâncias progressivamente preenchidas, útil para UIs onde você quer preencher um dashboard campo a campo:

from instructor import Partial

for partial_ticket in oi.chat.completions.create_partial(
    model="gpt-5",
    response_model=Partial[Ticket],
    messages=[{"role": "user", "content": prompt}],
):
    render_dashboard(partial_ticket)  # UI atualiza incremental

Erros comuns em produção (e como debugar)

Lista de sintomas que já apareceram nos meus clientes:

  • "Invalid schema: additionalProperties required to be false". Toda vez que você monta schema à mão. Solução: gere a partir de Pydantic com model.model_json_schema() e patch additionalProperties=False em todo type: object.
  • Modelo retorna null em um campo obrigatório. Em modelos que fazem best-effort (Claude 3, Gemini Flash), isso é comum quando o input é ambíguo. Adicione uma descrição explícita no schema tipo "Se incerto, escolha 'unknown', nunca null".
  • Latência 10x maior que o esperado. Você provavelmente está reenviando um schema novo a cada chamada. Cheque se o hash do schema é estável (evite gerar timestamps ou UUIDs dentro do schema).
  • Enum aceita valor fora da lista. Apenas Anthropic e Gemini em modelos antigos. Sempre valide no cliente com Pydantic Literal.
  • Custo explodindo em pipelines de extração. Structured outputs em documentos longos geram muitos tokens. Considere dividir: uma primeira chamada com structured output pequeno (só decide "qual seção do PDF é relevante"), depois uma chamada focada só na seção. Cortei 60% de custo em um projeto de due diligence jurídica assim.

Para observar essas falhas antes que virem incidente, instrumente cada chamada com trace (input tokens, output tokens, schema hash, tempo até primeiro byte, tempo total, tentativas de retry). Isso é ortogonal à sua escolha de biblioteca: Langfuse, Helicone e Arize aceitam metadados customizados.

Perguntas frequentes

Como forçar um LLM a retornar JSON válido?

Use structured outputs (OpenAI response_format=json_schema, Anthropic tool use com tool_choice forçada, Gemini responseSchema) em vez de prompt engineering. Structured outputs garantem sintaxe válida no servidor via constrained decoding; instruções no prompt são best-effort e falham silenciosamente em cerca de 2 a 5% dos casos.

Qual a diferença entre function calling e structured outputs?

Function calling é para o modelo escolher entre múltiplas ferramentas ou ações (agentes, tools). Structured outputs é para forçar o formato de uma única resposta final (extração, classificação). Ambos usam JSON Schema por baixo dos panos, mas a semântica de uso é diferente e o overhead de latência favorece structured outputs quando você não precisa de escolha.

Structured outputs aumentam a latência da API?

Sim, mas de forma limitada. A primeira chamada com um schema novo custa 200 a 800 ms extras na OpenAI (compilação do autômato). Chamadas seguintes com o mesmo schema voltam à latência normal. Tokens de output crescem 20 a 35% pelo overhead de JSON. Batch API neutraliza o custo se você aceita janela de resposta assíncrona.

Pydantic funciona com structured outputs da OpenAI?

Sim, é a integração canônica. Passe a classe Pydantic diretamente como response_format no cliente Python oficial e use completions.parse(). O SDK gera o JSON Schema, envia, e retorna uma instância validada em message.parsed. Bibliotecas como Instructor estendem isso para Anthropic e Gemini com a mesma API.

Qual biblioteca usar para structured outputs em Python?

Para casos simples e multi-provedor, use Instructor (leve, focado em validação e retry). Para agentes completos com dependências e tools, use Pydantic-AI. Se você só usa OpenAI, o SDK oficial já basta. Evite fazer parsing manual com regex; é a fonte #1 de bugs em produção que já vi.

Structured outputs funcionam com modelos open-source?

Sim, mas via backends de inferência específicos. vLLM suporta constrained decoding via outlines ou lm-format-enforcer. llama.cpp aceita gramática GBNF. Ollama expõe format=json mas sem schema arbitrário. Para schema estrito, prefira vLLM em produção com Llama 3.3, Qwen 2.5 ou Mistral Large.

Sobre o Autor Marcus Holloway

Marcus has been gluing systems together for twelve years - first as an integrations engineer at Tray.io, then four years at MuleSoft (post-Salesforce acquisition) leading a team that built connectors for regulated-industry customers. He moved full-time into LLM orchestration in 2023 after a side project - an n8n workflow that triaged his consulting firm's intake email - replaced an actual headcount. He focuses on the boring middle layer: idempotent webhook receivers, dead-letter queues for tool-call failures, and getting Temporal to play nicely with OpenAI's Assistants API. He's published two open-source n8n community nodes (one for Pinecone hybrid search, one for Anthropic prompt caching) and contributed retry-backoff improvements to the LangChain JS repo. Lives in Atlanta. Writes about what actually breaks in production agents, not what looks good in a demo.