Structured Output de LLMs em 2026: Para de Parsear JSON com Regex e Faz Direito
Cê já passou por isso. Pede pro GPT "me devolve um objeto JSON com o nome, email e score de sentimento do usuário." Ele devolve um JSON perfeitamente formatado... dentro de um bloco de código markdown. Com uma explicação solícita. E um disclaimer sobre como ele é uma IA.
Aí você escreve um regex pra tirar os code fences. Depois outro regex pro comentário no final. Depois ele devolve JSONL em vez de JSON. Depois embrulha tudo em {"result": ...} sem ninguém ter pedido. Funciona perfeito por 10.000 requests e explode catastroficamente no request 10.001 porque o nome do usuário tinha um caractere de aspas.
Esse é o problema do structured output, e em 2026, não tem mais motivo pra resolver isso na mão.
Todos os provedores principais de LLM agora oferecem structured output nativo. As ferramentas (Pydantic pro Python, Zod pro TypeScript) amadureceram enormemente. Mesmo assim, a maioria dos devs ainda tá parseando strings cruas ou usando function calling como gambiarra.
Este guia cobre tudo: como structured output funciona por debaixo dos panos, como implementar no OpenAI, Anthropic e Gemini, os ecossistemas Python e TypeScript, e — o mais importante — as armadilhas de produção que vão te morder se você não conhecer.
Por Que Structured Output Importa (Mais Do Que Você Imagina)
O problema fundamental com LLMs em produção:
LLMs são geradores de texto.
Sua aplicação precisa de estruturas de dados.
No espaço entre os dois é onde moram os bugs.
Quando você faz JSON.parse() numa resposta crua de LLM, tá fazendo várias suposições perigosas:
- A saída é JSON válido (pode não ser)
- O JSON tem os campos que você espera (pode não ter)
- Os tipos dos campos estão certos (strings vs numbers vs booleans)
- Os valores estão dentro dos ranges esperados (sentiment: -1 a 1, não "positive")
- A resposta não contém campos extras que você não pediu
- O formato é consistente com diferentes inputs
Structured output elimina todos os seis problemas restringindo a saída do modelo no nível de geração de tokens — não depois.
Os Três Níveis de Controle de Saída
Nível 1: Prompt Engineering (Não confiável)
"Devolve JSON com campos: name, email, score"
→ Funciona 80-95% das vezes
→ Falha silenciosamente em edge cases
→ Sem garantias de tipo
Nível 2: Function Calling / Tool Use (Melhor)
Define um schema de função, o modelo "chama"
→ Funciona 95-99% das vezes
→ Schema é uma dica, não uma restrição
→ Pode produzir valores inválidos dentro de tipos válidos
Nível 3: Native Structured Output (O melhor)
Decodificação restrita com JSON Schema
→ Funciona 100% das vezes (validade do schema garantida)
→ Usa máquinas de estado finito pra mascarar tokens inválidos
→ Tipos E valores forçados no momento da geração
Se é produção, tem que estar no Nível 3.
Como Structured Output Funciona de Verdade
A maioria dos devs trata como caixa preta: "Passo um schema, recebo JSON válido." Mas entender o mecanismo importa pra debugging e otimização.
Decodificação Restrita (A Mágica Por Trás da Cortina)
Quando um LLM gera texto, ele prevê o próximo token de um vocabulário de ~100.000+ tokens. Normalmente, qualquer token pode vir depois de outro. Structured output adiciona uma camada de restrição:
Geração normal:
Probabilidades: {"hello": 0.3, "{": 0.1, "The": 0.2, ...}
→ Qualquer token pode ser selecionado
Geração restrita (esperando início de objeto JSON):
Probabilidades: {"hello": 0.3, "{": 0.1, "The": 0.2, ...}
Máscara: {"hello": 0, "{": 1, "The": 0, ...}
→ Só "{" e tokens de whitespace são válidos
→ O modelo TEM que produzir "{"
Isso é implementado com uma Máquina de Estado Finito (FSM) que rastreia onde você tá no JSON schema:
FSM pra {"name": string, "age": integer}:
INÍCIO → esperar "{"
→ esperar "\"name\""
→ esperar ":"
→ esperar valor string
→ esperar "," ou "}"
→ se ",": esperar "\"age\""
→ esperar ":"
→ esperar valor inteiro
→ esperar "}"
→ FIM
Em cada estado, a FSM mascara todos os tokens que violariam o schema. O modelo continua escolhendo o token válido mais provável, preservando qualidade enquanto garante estrutura.
Por Que É Melhor que Prompt Engineering
Prompt: "Devolve um objeto JSON com 'score' como número entre 0 e 1"
Sem decodificação restrita:
Pode vir: {"score": "0.85"} ← string, não número
Pode vir: {"score": 0.85, "confidence": "high"} ← campo extra
Pode vir: {"score": 85} ← fora do range
Pode vir: Claro! Aqui está o JSON: {"score": 0.85} ← preâmbulo
Com decodificação restrita:
Sempre vem: {"score": 0.85} ← sempre válido
Implementação: OpenAI
O structured output do OpenAI é o mais maduro. Tá disponível na API de Chat Completions com response_format.
Uso Básico
from openai import OpenAI from pydantic import BaseModel client = OpenAI() class SentimentAnalysis(BaseModel): sentiment: str # "positive", "negative", "neutral" confidence: float key_phrases: list[str] reasoning: str response = client.beta.chat.completions.parse( model="gpt-5-mini", messages=[ {"role": "system", "content": "Analise o sentimento do texto dado."}, {"role": "user", "content": "Esse produto é absolutamente terrível. Pior compra da minha vida."} ], response_format=SentimentAnalysis, ) result = response.choices[0].message.parsed print(result.sentiment) # "negative" print(result.confidence) # 0.95 print(result.key_phrases) # ["absolutamente terrível", "pior compra"]
Com Enums e Objetos Aninhados
from enum import Enum from pydantic import BaseModel, Field class Sentiment(str, Enum): positive = "positive" negative = "negative" neutral = "neutral" mixed = "mixed" class Entity(BaseModel): name: str type: str = Field(description="person, organization, product, or location") sentiment: Sentiment class FullAnalysis(BaseModel): overall_sentiment: Sentiment confidence: float = Field(ge=0.0, le=1.0) entities: list[Entity] summary: str = Field(max_length=200) topics: list[str] = Field(min_length=1, max_length=5) response = client.beta.chat.completions.parse( model="gpt-5-mini", messages=[ {"role": "system", "content": "Extraia análise estruturada do texto."}, {"role": "user", "content": "O novo MacBook Pro da Apple é incrível, mas o keynote do Tim Cook foi entediante."} ], response_format=FullAnalysis, ) result = response.choices[0].message.parsed # result.entities = [ # Entity(name="Apple", type="organization", sentiment="positive"), # Entity(name="MacBook Pro", type="product", sentiment="positive"), # Entity(name="Tim Cook", type="person", sentiment="negative"), # ]
TypeScript + Zod
import OpenAI from 'openai'; import { z } from 'zod'; import { zodResponseFormat } from 'openai/helpers/zod'; const client = new OpenAI(); const SentimentSchema = z.object({ sentiment: z.enum(['positive', 'negative', 'neutral', 'mixed']), confidence: z.number().min(0).max(1), entities: z.array(z.object({ name: z.string(), type: z.enum(['person', 'organization', 'product', 'location']), sentiment: z.enum(['positive', 'negative', 'neutral']), })), summary: z.string(), topics: z.array(z.string()).min(1).max(5), }); type Sentiment = z.infer<typeof SentimentSchema>; const response = await client.beta.chat.completions.parse({ model: 'gpt-5-mini', messages: [ { role: 'system', content: 'Extraia análise estruturada do texto.' }, { role: 'user', content: 'O novo compilador do React é demais mas a documentação de migração tá fraca.' }, ], response_format: zodResponseFormat(SentimentSchema, 'sentiment_analysis'), }); const result: Sentiment = response.choices[0].message.parsed!; console.log(result.sentiment); // "mixed"
Implementação: Anthropic (Claude)
A Anthropic usa tool use (function calling) como mecanismo pro structured output. Você define uma ferramenta com um JSON schema, e o Claude devolve dados estruturados como se tivesse chamando essa ferramenta.
Uso Básico
import anthropic from pydantic import BaseModel client = anthropic.Anthropic() class ExtractedData(BaseModel): name: str email: str company: str role: str urgency: str # "low", "medium", "high", "critical" response = client.messages.create( model="claude-sonnet-4-20260514", max_tokens=1024, tools=[{ "name": "extract_contact", "description": "Extrair informações de contato do email.", "input_schema": ExtractedData.model_json_schema(), }], tool_choice={"type": "tool", "name": "extract_contact"}, messages=[{ "role": "user", "content": """Extrai as infos de contato desse email: Oi, sou Sarah Chen da DataFlow Inc. Nosso pipeline de produção tá fora do ar e precisamos de ajuda imediata. Me contata em [email protected] — sou VP de Engineering.""", }], ) tool_result = next( block for block in response.content if block.type == "tool_use" ) data = ExtractedData(**tool_result.input) print(data.name) # "Sarah Chen" print(data.urgency) # "critical"
TypeScript + Zod + Anthropic
import Anthropic from '@anthropic-ai/sdk'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; const client = new Anthropic(); const ContactSchema = z.object({ name: z.string(), email: z.string().email(), company: z.string(), role: z.string(), urgency: z.enum(['low', 'medium', 'high', 'critical']), }); const response = await client.messages.create({ model: 'claude-sonnet-4-20260514', max_tokens: 1024, tools: [{ name: 'extract_contact', description: 'Extrair informações de contato do email.', input_schema: zodToJsonSchema(ContactSchema) as Anthropic.Tool.InputSchema, }], tool_choice: { type: 'tool' as const, name: 'extract_contact' }, messages: [{ role: 'user', content: 'Extrai: Oi, sou John Park, CTO da Acme Corp ([email protected]). Não é urgente.', }], }); const toolBlock = response.content.find( (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use' ); const data = ContactSchema.parse(toolBlock!.input); console.log(data.urgency); // "low"
Implementação: Google Gemini
O Gemini suporta structured output nativamente pelo parâmetro response_schema. Usa decodificação restrita similar ao OpenAI.
Uso Básico
import google.generativeai as genai from pydantic import BaseModel from enum import Enum class Priority(str, Enum): low = "low" medium = "medium" high = "high" critical = "critical" class TaskExtraction(BaseModel): title: str assignee: str priority: Priority deadline: str | None tags: list[str] model = genai.GenerativeModel( "gemini-2.5-flash", generation_config=genai.GenerationConfig( response_mime_type="application/json", response_schema=TaskExtraction, ), ) response = model.generate_content( "Extrai a task: 'John precisa arrumar o bug de login até sexta. Tá bloqueando prod. Tagueia como backend e auth.'" ) import json result = TaskExtraction(**json.loads(response.text)) print(result.priority) # "critical" print(result.tags) # ["backend", "auth"]
Tabela Comparativa de Provedores
Antes de escolher provedor pra structured output, olha como eles se comparam:
Feature OpenAI Anthropic Gemini
───────────────── ────────────── ────────────── ──────────────
Método SO Nativo Tool Use SO Nativo
Decode restrito Sim Parcial Sim
100% schema válido Garantido 99%+ Garantido
Streaming Sim Sim Sim
Pydantic nativo .parse() Schema manual Schema manual
Zod nativo Helper Conversão manual Conversão manual
Objetos aninhados Sim Sim Sim
Enums Sim Sim Sim
Campos opcionais Sim Sim Sim
Schemas recursivos Limitado Sim Limitado
Max depth schema 5 níveis Sem limite Sem limite
Handling de refusal Sim N/A N/A
Recomendação: Se precisa de compliance 100% do schema, usa o structured output nativo do OpenAI ou Gemini. Se já tá no Claude, o padrão de tool use funciona bem, mas adiciona validação com Pydantic/Zod como rede de segurança.
Padrões de Produção Que Realmente Funcionam
Padrão 1: O Sanduíche de Validação
Nunca confia na saída do LLM diretamente, nem com structured output. Sempre valida.
from pydantic import BaseModel, Field, field_validator from openai import OpenAI client = OpenAI() class ProductReview(BaseModel): rating: int = Field(ge=1, le=5) title: str = Field(min_length=5, max_length=100) pros: list[str] = Field(min_length=1, max_length=5) cons: list[str] = Field(max_length=5) would_recommend: bool @field_validator('title') @classmethod def title_not_generic(cls, v: str) -> str: generic_titles = ['good', 'bad', 'ok', 'fine', 'great'] if v.lower().strip() in generic_titles: raise ValueError(f'Título muito genérico: {v}') return v def extract_review(text: str) -> ProductReview: response = client.beta.chat.completions.parse( model="gpt-5-mini", messages=[ {"role": "system", "content": "Extraia uma review de produto estruturada."}, {"role": "user", "content": text}, ], response_format=ProductReview, ) result = response.choices[0].message.parsed if response.choices[0].message.refusal: raise ValueError(f"Modelo recusou: {response.choices[0].message.refusal}") # Re-valida mesmo com garantia de schema do OpenAI # (pega violações de lógica de negócio que JSON Schema não consegue expressar) return ProductReview.model_validate(result.model_dump())
Padrão 2: Retry com Escalação
Structured output falhar é raro, mas acontece. Escala com graça:
from tenacity import retry, stop_after_attempt, wait_exponential @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), ) def extract_with_retry(text: str, schema: type[BaseModel]) -> BaseModel: try: response = client.beta.chat.completions.parse( model="gpt-5-mini", messages=[ {"role": "system", "content": "Extraia dados estruturados com precisão."}, {"role": "user", "content": text}, ], response_format=schema, ) result = response.choices[0].message.parsed return schema.model_validate(result.model_dump()) except Exception as e: print(f"Tentativa falhou: {e}") raise try: review = extract_with_retry(user_text, ProductReview) except Exception: review = extract_with_retry(user_text, SimpleReview)
Padrão 3: Fallback Multi-Provedor
Não se amarra num provedor só. Monta uma cadeia de fallback:
import OpenAI from 'openai'; import Anthropic from '@anthropic-ai/sdk'; import { z } from 'zod'; import { zodResponseFormat } from 'openai/helpers/zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; const schema = z.object({ intent: z.enum(['question', 'complaint', 'feedback', 'request']), urgency: z.enum(['low', 'medium', 'high']), summary: z.string().max(200), action_required: z.boolean(), }); type TicketClassification = z.infer<typeof schema>; async function classifyTicket(text: string): Promise<TicketClassification> { // Tenta OpenAI primeiro (structured output mais rápido) try { const openai = new OpenAI(); const response = await openai.beta.chat.completions.parse({ model: 'gpt-5-mini', messages: [ { role: 'system', content: 'Classifique o ticket de suporte.' }, { role: 'user', content: text }, ], response_format: zodResponseFormat(schema, 'ticket'), }); return schema.parse(response.choices[0].message.parsed); } catch (openaiError) { console.warn('OpenAI falhou, caindo pro Claude:', openaiError); } // Fallback pra Anthropic try { const anthropic = new Anthropic(); const response = await anthropic.messages.create({ model: 'claude-sonnet-4-20260514', max_tokens: 512, tools: [{ name: 'classify', description: 'Classificar o ticket.', input_schema: zodToJsonSchema(schema) as Anthropic.Tool.InputSchema, }], tool_choice: { type: 'tool' as const, name: 'classify' }, messages: [{ role: 'user', content: `Classifique: ${text}` }], }); const toolBlock = response.content.find( (b): b is Anthropic.ToolUseBlock => b.type === 'tool_use' ); return schema.parse(toolBlock!.input); } catch (anthropicError) { console.error('Os dois provedores falharam:', anthropicError); throw new Error('Todos os provedores de LLM falharam pro structured output'); } }
Padrão 4: Streaming Structured Output
Pra respostas estruturadas longas, transmita resultados parciais:
from openai import OpenAI from pydantic import BaseModel client = OpenAI() class Article(BaseModel): title: str sections: list[dict] # {"heading": str, "content": str} tags: list[str] word_count: int with client.beta.chat.completions.stream( model="gpt-5", messages=[ {"role": "system", "content": "Gere um outline de artigo com seções detalhadas."}, {"role": "user", "content": "Escreva sobre WebAssembly em 2026."} ], response_format=Article, ) as stream: for event in stream: snapshot = event.snapshot if snapshot and snapshot.choices[0].message.content: partial = snapshot.choices[0].message.content print(f"Recebendo: {len(partial)} chars...") final = stream.get_final_completion() article = final.choices[0].message.parsed print(f"Artigo: {article.title} ({article.word_count} palavras)")
As Armadilhas Que Ninguém Te Conta
Armadilha 1: O Imposto da Complexidade do Schema
Cada restrição que você adiciona ao schema aumenta a latência. Schemas complexos com objetos profundamente aninhados podem dobrar ou triplicar o tempo de resposta.
Complexidade do schema vs latência (gpt-5-mini, média):
Schema tok/s Primeiro Token Tempo Total
───────────────────────────── ────────── ────────────── ──────────
Sem schema (texto livre) 85 tok/s ~200ms ~500ms
Simples (3 campos) 78 tok/s ~250ms ~550ms
Médio (10 campos, 1 enum) 65 tok/s ~350ms ~800ms
Complexo (20+ campos, nested) 45 tok/s ~500ms ~1.5s
Muito complexo (recursivo) 30 tok/s ~800ms ~3s
Solução: Quebra schemas complexos em múltiplas chamadas menores:
# ❌ Um schema gigante class FullDocumentAnalysis(BaseModel): entities: list[Entity] # 20+ campos cada sentiment: SentimentDetail # 10+ campos summary: Summary # 5 campos classification: Classification # 8 campos # ✅ Pipeline de schemas pequenos class Step1_Entities(BaseModel): entities: list[SimpleEntity] class Step2_Sentiment(BaseModel): overall: str confidence: float aspects: list[str] class Step3_Classification(BaseModel): category: str subcategory: str priority: str import asyncio entities, sentiment, classification = await asyncio.gather( extract(text, Step1_Entities), extract(text, Step2_Sentiment), extract(text, Step3_Classification), )
Armadilha 2: O Inferno do Versionamento de Schema
Sua app evolui. Seu schema evolui. Mas o LLM não sabe que você renomeou user_name pra name terça passada.
# Versão 1 (deploy em janeiro) class UserProfile_v1(BaseModel): user_name: str email_address: str age: int # Versão 2 (deploy em fevereiro) class UserProfile_v2(BaseModel): name: str # renomeado! email: str # renomeado! age: int location: str | None # campo novo
Solução: Usa versionamento explícito de schema e migração:
from pydantic import BaseModel, Field from typing import Literal class UserProfile(BaseModel): schema_version: Literal["2.0"] = "2.0" name: str = Field(alias="user_name") email: str = Field(alias="email_address") age: int location: str | None = None class Config: populate_by_name = True
Armadilha 3: A Cilada do Array Vazio
LLMs odeiam devolver arrays vazios quando genuinamente não tem nada pra extrair. Eles vão alucinar entradas pra "preencher" o array.
# Input: "O tempo tá bom hoje." # Esperado: {"entities": [], "topics": ["clima"]} # Real: {"entities": [{"name": "tempo", "type": "concept"}], "topics": ["clima"]} # Solução: Deixa explícito que arrays vazios são válidos class Extraction(BaseModel): entities: list[Entity] = Field( default_factory=list, description="Entidades nomeadas no texto. Devolva lista vazia [] se não encontrar nenhuma." )
Armadilha 4: Confusão de Enums com Valores Parecidos
class Priority(str, Enum): low = "low" medium = "medium" high = "high" critical = "critical" urgent = "urgent" # ← Qual a diferença de "critical"? # O modelo vai ficar escolhendo inconsistentemente entre "critical" e "urgent" # porque NEM ELE sabe a diferença. # Solução: Use menos valores de enum, claramente distintos + descrições class Priority(str, Enum): low = "low" # Dá pra esperar dias/semanas medium = "medium" # Tem que resolver nesse sprint high = "high" # Precisa de atenção hoje critical = "critical" # Produção caiu, arruma AGORA
Armadilha 5: Limites de Token e Truncamento
Structured output não ignora limites de token. Se o modelo bate o max_tokens antes de completar o JSON, você recebe JSON inválido.
Solução: Sempre coloca max_tokens bem maior que o output esperado e trata o finish_reason:
response = client.beta.chat.completions.parse( model="gpt-5-mini", messages=[...], response_format=MySchema, max_tokens=4096, ) if response.choices[0].finish_reason == "length": raise ValueError("Resposta truncada — aumenta max_tokens ou simplifica o schema")
Armadilha 6: A Cilada do Refusal (Só OpenAI)
O structured output do OpenAI pode recusar gerar conteúdo se o input acionar filtros de segurança.
response = client.beta.chat.completions.parse( model="gpt-5-mini", messages=[ {"role": "user", "content": "Analisa essa reclamação do cliente: [conteúdo potencialmente sensível]"} ], response_format=Analysis, ) parsed = response.choices[0].message.parsed refusal = response.choices[0].message.refusal if refusal: print(f"Modelo recusou: {refusal}") elif parsed: process(parsed)
Pydantic vs Zod: A Comparação Definitiva
Feature Pydantic (Python) Zod (TypeScript)
─────────────────────── ──────────────────── ────────────────────
Inferência de tipo De anotações De .infer<>
Validação runtime Built-in Built-in
Export JSON Schema .model_json_schema() zodToJsonSchema()
Valores default Field(default=...) .default(value)
Validators custom @field_validator .refine() / .transform()
Objetos aninhados Nativo Nativo
Unions discriminadas Suportado .discriminatedUnion()
Schemas recursivos Suportado z.lazy()
Serialização .model_dump() N/A (objetos planos)
Integração ORM Sim (SQLAlchemy) Drizzle/Prisma
Suporte nativo OpenAI .parse() zodResponseFormat
Integração Anthropic .model_json_schema() zodToJsonSchema()
Quando Usar Pydantic
from pydantic import BaseModel, Field, field_validator, model_validator class Invoice(BaseModel): items: list[LineItem] subtotal: float tax_rate: float = Field(ge=0, le=0.5) total: float @model_validator(mode='after') def validate_total(self) -> 'Invoice': expected = self.subtotal * (1 + self.tax_rate) if abs(self.total - expected) > 0.01: raise ValueError( f'Total {self.total} não bate com ' f'subtotal {self.subtotal} × (1 + {self.tax_rate}) = {expected}' ) return self
Quando Usar Zod
const InvoiceSchema = z.object({ items: z.array(LineItemSchema), subtotal: z.number().positive(), taxRate: z.number().min(0).max(0.5), total: z.number().positive(), }).refine( (data) => Math.abs(data.total - data.subtotal * (1 + data.taxRate)) < 0.01, { message: 'total não bate com subtotal × (1 + taxRate)' } ); type Invoice = z.infer<typeof InvoiceSchema>;
Padrão Avançado: Composição de Schemas pra Workflows Complexos
Aplicações reais raramente precisam de um schema só. Olha como compor schemas pra um pipeline de extração multi-etapa:
from pydantic import BaseModel, Field from enum import Enum from openai import OpenAI client = OpenAI() # Etapa 1: Classificação rápida (modelo rápido e barato) class TicketType(str, Enum): bug = "bug" feature = "feature" question = "question" billing = "billing" class QuickClassification(BaseModel): type: TicketType language: str = Field(description="Linguagem de programação se aplicável, senão 'N/A'") needs_human: bool # Etapa 2: Extração detalhada (só pra bugs, modelo mais esperto) class BugReport(BaseModel): title: str = Field(max_length=100) steps_to_reproduce: list[str] = Field(min_length=1) expected_behavior: str actual_behavior: str environment: dict[str, str] severity: str = Field(description="minor, major, or critical") # Etapa 3: Roteamento automático class RoutingDecision(BaseModel): team: str = Field(description="backend, frontend, infra, or billing") priority: int = Field(ge=1, le=5) suggested_assignee: str | None auto_reply: str = Field(max_length=500) async def process_ticket(text: str): classification = await extract(text, QuickClassification, model="gpt-5-mini") if classification.needs_human: return route_to_human(text) details = None if classification.type == TicketType.bug: details = await extract(text, BugReport, model="gpt-5") context = f"Tipo: {classification.type}. " if details: context += f"Severidade: {details.severity}. Passos: {details.steps_to_reproduce}" routing = await extract(context, RoutingDecision, model="gpt-5-mini") return { "classification": classification, "details": details, "routing": routing, }
Otimização de Custos: Structured Output Não É de Graça
Structured output adiciona overhead:
Fatores de custo:
1. Tokens de schema: O JSON schema é incluído no system prompt
Schema simples (3 campos): ~50 tokens ($0.00001)
Complexo (20 campos): ~500 tokens ($0.0001)
Muito complexo (nested): ~2000 tokens ($0.0004)
2. Tokens de output: SO gera mais tokens que texto livre
"O sentimento é positivo" = 5 tokens
{"sentiment": "positive"} = 7 tokens (~40% mais)
3. Overhead de latência: ~10-30% a mais
Impacto mensal (1M requests/dia):
────────────────────────────────────────────
Abordagem Tokens/req Custo/mês Latência
Texto livre 50 $1,500 200ms
Structured simples 70 $2,100 250ms
Structured complexo 200 $6,000 400ms
Estratégia de economia:
→ Usa SO só onde realmente precisa
→ Texto livre pra resumos, structured pra extração
→ Cache agressivo de respostas
→ gpt-5-mini pra classificação, gpt-5 pra extração complexa
Framework de Decisão
Nem tudo precisa de structured output:
Use Structured Output quando:
✅ Output vai direto pro código (API responses, inserts no DB)
✅ Precisa de garantias de tipo (números têm que ser números)
✅ Múltiplos consumidores dependem de formato consistente
✅ Tá construindo pipelines automatizados (sem humano no loop)
✅ Extração de dados de texto não estruturado
Não use Structured Output quando:
❌ Output é mostrado direto pro usuário (chat, geração de conteúdo)
❌ Precisa de respostas criativas e livres
❌ O schema seria mais complexo que a tarefa
❌ Tá prototipando e o schema muda todo dia
❌ Custo é prioridade e texto livre funciona bem
O Que Vem Pela Frente
2026 Q1-Q2 (Agora)
- ✅ Structured output do OpenAI GA com streaming
- ✅ Tool use da Anthropic estável no Claude Sonnet/Opus
- ✅ Gemini 2.5 modo JSON nativo com enforcement de schema
- 🔄 Pydantic v3 beta com hooks nativos pra LLM
- 🔄 Zod v4 com melhor compatibilidade JSON Schema
2026 Q3-Q4
- Portabilidade de schemas cross-provider (um schema, qualquer LLM)
- Streaming de objetos parciais com callbacks por campo
- Auto-geração de schemas a partir de interfaces TypeScript
- Decodificação restrita pra imagens e áudio (SO multimodal)
2027 e Além
- Structured output vira o padrão (texto livre vira opt-in)
- LLMs que negociam mudanças de schema em runtime
- Validação embutida diretamente nos pesos do modelo
Conclusão
Structured output em 2026 não é mais opcional pra aplicações LLM em produção. Os dias de parsear respostas do GPT com regex e rezar acabaram.
Os pontos-chave:
- Use structured output nativo (
.parse()do OpenAI,response_schemado Gemini). Não cria seu próprio parser de JSON. - Sempre valide com Pydantic ou Zod, mesmo quando o provedor garante compliance do schema. Validação de lógica de negócio pega o que JSON Schema não consegue.
- Fica de olho nos custos. Schemas complexos são caros. Quebra em chamadas menores e paralelas.
- Trata edge cases: refusals, truncamento, arrays vazios e confusão de enums vão te morder em produção.
- Monta cadeias de fallback. Nenhum provedor é 100% confiável. Usa padrões multi-provedor pra paths críticos.
A pergunta real não é "devo usar structured output?" É "por que cê ainda tá parseando texto livre com regex em 2026?"
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit