Peça a um modelo para responder em JSON e na maioria das vezes json.loads() funciona sem problemas. As falhas são as que custam uma sessão de debug: um comentário de uma linha dentro do objeto, um code fence de markdown envolvendo tudo, uma chave escrita userEmail quando seu código espera user_email. Um prompt mais estrito (“responda APENAS com o objeto JSON, sem nenhum outro texto”) torna esses casos mais raros. Não os torna impossíveis, porque o modelo ainda pode ignorar o pedido. A saída estruturada remove o pedido da equação: você entrega à API um JSON Schema, que restringe a amostragem de tokens do próprio modelo para que a resposta tenha que corresponder a ele.

JSON mode contra saída estruturada: o que cada um garante

O “JSON mode” (response_format: {"type": "json_object"} na API antiga da OpenAI, e o equivalente na maioria dos outros provedores) garante uma única coisa: a saída é interpretável como JSON. Não diz nada sobre quais chaves existem, que tipo cada uma tem, ou se um campo do qual você depende está mesmo presente. Um modelo em JSON mode pode devolver {"result": "yes"} quando seu código espera {"status": "approved", "confidence": 0.91}, e nada avisa que algo deu errado.

A saída estruturada (a OpenAI chama de Structured Outputs, a Anthropic chama de JSON outputs) é uma garantia diferente: a resposta corresponde ao JSON Schema que você fornece, com as chaves obrigatórias presentes e o tipo correto em cada uma. O function calling resolve um problema adjacente: fazer o modelo pedir a seu código que execute algo. Os dois se confundem com facilidade porque ambos passam um esquema. O que muda é o que o esquema molda:

RecursoO que garantePara que serve
JSON mode (json_object)Apenas sintaxe JSON válidaFallback legado, sem esquema a cumprir
Saída estruturada (schema strict)O JSON corresponde exatamente ao seu esquemaMoldar a resposta para o usuário ou seu código
Function calling / tool useNome e argumentos do tool correspondem ao seu esquemaO modelo decide invocar seu código

Um pipeline em produção costuma usar os dois últimos juntos. O tool use strict evita que as chamadas de função de um agente cheguem malformadas, e um esquema strict na resposta final tipa o resumo devolvido ao usuário.

Como o constrained decoding força a correspondência

O modelo nunca compara sua resposta com seu esquema depois de gerá-la. O esquema muda o que ele tem permissão para gerar, um token por vez.

A camada de serving compila seu JSON Schema em uma gramática: uma máquina de estados que rastreia, em cada posição da saída, quais tokens ainda levariam a algo válido. Antes de cada token ser amostrado, todo token do vocabulário que a gramática descarta naquela posição tem seu logit empurrado para menos infinito, ficando com probabilidade zero depois do softmax. O modelo amostra do que resta, e as probabilidades relativas entre os tokens sobreviventes são preservadas. Uma chave escrita errada, uma chave de fechamento no lugar errado, uma string onde o esquema exige um número: uma vez que a gramática os proíbe, esses tokens deixam de ser opções.

Essa etapa de compilação tem um custo. Construir uma gramática para um novo esquema exige trabalho real, então a primeira requisição contra aquele esquema é mais lenta que as seguintes. A API da Anthropic mantém a gramática compilada em cache por 24 horas após o último uso, e chamadas posteriores com o mesmo esquema pulam a compilação.

O mecanismo também explica o limite. O constrained decoding fixa apenas a forma, nada mais. A gramática sabe quais tokens são sintaticamente legais, não qual deles é verdadeiro, então o modelo ainda pode preencher um campo "email" válido com um endereço inventado.

Como obter JSON conforme o esquema na OpenAI, Claude e Ollama

A Responses API da OpenAI recebe o esquema em text.format, com strict ativado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-6-astra",
    input="Extract the ticket details: 'Login fails with a 500 after the "
          "password reset flow, seen first on build 4.12, blocking release.'",
    text={
        "format": {
            "type": "json_schema",
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "summary": {"type": "string"},
                    "build": {"type": "string"},
                    "severity": {"type": "string", "enum": ["low", "medium", "high", "blocking"]},
                },
                "required": ["summary", "build", "severity"],
                "additionalProperties": False,
            },
        }
    },
)
print(response.output_text)

O Claude recebe o esquema em output_config.format, não em uma definição de tool:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
curl https://api.anthropic.com/v1/messages \
  -H "content-type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -d '{
    "model": "claude-sonnet-5-5",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Extract the ticket details: ..."}],
    "output_config": {
      "format": {
        "type": "json_schema",
        "schema": {
          "type": "object",
          "properties": {
            "summary": {"type": "string"},
            "build": {"type": "string"},
            "severity": {"type": "string", "enum": ["low", "medium", "high", "blocking"]}
          },
          "required": ["summary", "build", "severity"],
          "additionalProperties": false
        }
      }
    }
  }'

O JSON volta no conteúdo de texto da resposta, não em um bloco de tool call. O workaround antigo (definir um tool falso, forçar o modelo a chamá-lo, ler os argumentos) ainda funciona em modelos que só suportam tool use. Onde existe saída estruturada nativa, você não precisa disso: o decoder impõe o esquema diretamente, em vez de esperar que os argumentos do tool coincidam por acaso.

Um modelo executado localmente com Ollama recebe o esquema pelo campo format de /api/chat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
curl http://localhost:11434/api/chat -d '{
  "model": "llama3.2",
  "messages": [{"role": "user", "content": "Extract the ticket details: ..."}],
  "format": {
    "type": "object",
    "properties": {
      "summary": {"type": "string"},
      "build": {"type": "string"},
      "severity": {"type": "string", "enum": ["low", "medium", "high", "blocking"]}
    },
    "required": ["summary", "build", "severity"]
  },
  "stream": false
}'

O mecanismo é o mesmo. O runtime llama.cpp do Ollama constrói uma gramática a partir do esquema e mascara os tokens do mesmo jeito que as APIs hospedadas.

Quais recursos de JSON Schema os provedores suportam

Cada provedor impõe um subconjunto de JSON Schema, e os subconjuntos diferem o bastante para quebrar um esquema copiado de uma API para outra.

A OpenAI exige que todo objeto leve "additionalProperties": false com cada uma de suas propriedades listada em required — não há como marcar um campo como realmente opcional, então modele-o como required e nullable. A Anthropic é mais permissiva: uma propriedade fora de required é genuinamente opcional, e as propriedades required aparecem primeiro na saída, seguidas das opcionais presentes. A Anthropic também suporta $ref/$defs locais, anyOf e const, mas rejeita esquemas recursivos, limites numéricos como minimum e maximum, e restrições de tamanho de string como maxLength. Os exemplos da OpenAI incluem um $ref: "#" recursivo para saída em árvore, algo que a documentação da Anthropic exclui explicitamente.

Quando seu esquema precisa de uma restrição que o provedor não suporta, mova-a para a description do campo (“deve ser um inteiro positivo menor que 100”) e verifique você mesmo depois do parsing. Os helpers dos SDKs de ambas as plataformas fazem isso automaticamente quando você constrói o esquema a partir de um modelo Pydantic ou Zod em vez de escrever JSON Schema na mão.

Como validar a saída estruturada depois do parsing

Uma forma válida não é uma resposta correta. O esquema garante que severity seja um dos seus quatro valores de enum. Se “blocking” é a decisão certa para este ticket é outra pergunta. Trate o objeto parseado como você trataria o envio de um formulário: input a validar, não verdade estabelecida.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pydantic import BaseModel, field_validator

class Ticket(BaseModel):
    summary: str
    build: str
    severity: str

    @field_validator("build")
    @classmethod
    def must_look_like_a_build_number(cls, v: str) -> str:
        if not v.replace(".", "").isdigit():
            raise ValueError(f"'{v}' doesn't look like a build number")
        return v

O Instructor envolve a OpenAI, a Anthropic e a maioria dos outros provedores em um ciclo de validação e retry. Você passa a ele um modelo Pydantic. Em uma falha de validação, ele devolve o erro ao modelo e pergunta de novo, até um limite de retry que você define, em vez de lançar um ValidationError na primeira falha. Também é o caminho prático para conseguir uma garantia de esquema em um provedor ou modelo mais antigo sem strict mode nativo: o ciclo de retry substitui o enforcement no nível do decoder, e cada falha custa uma viagem de ida e volta extra.

Os validadores de campo pegam valores malformados em uma única resposta. Para verificar se as respostas estão corretas em centenas de entradas, e se uma mudança no prompt as piorou, você precisa de um eval suite.

Quando a saída estruturada falha de qualquer forma

O strict mode não cobre esses casos. Verifique-os antes de sair caçando um bug no seu próprio código:

  • Recusas. Um modelo pode se recusar a responder, e o texto da recusa não corresponde ao seu esquema. Verifique stop_reason (ou o equivalente) antes do parsing, não depois que o parsing lançar um erro.
  • Truncamento. Atingir max_tokens no meio de um objeto produz JSON incompleto. A gramática controla qual token vem a seguir, não quanto espaço resta para terminar. Aumente o limite ou reduza o esquema.
  • Caminhos não forçados. JSON mode, uma biblioteca de retry em um modelo sem strict mode, ou um provedor que ignora silenciosamente uma palavra-chave do esquema que não suporta podem todos devolver "Blocking" quando seu enum diz "blocking". Se alguma requisição do seu sistema passar por um desses caminhos, normalize os valores na entrada.
  • Tamanho do esquema. Um esquema grande com muitos ramos anyOf adiciona tokens a cada requisição, além do atraso de compilação para um esquema novo. Mantenha os esquemas tão simples e pequenos quanto a tarefa permitir.

A garantia do provedor é real, mas seu código de parsing não deve assumir que ela se manteve.

JSON mode, schema strict ou function calling: qual usar

Use o JSON mode simples só contra um modelo ou caminho de SDK sem suporte a esquema, como fallback e não como padrão. Use um JSON Schema strict quando estiver moldando uma resposta para seu próprio código ou um usuário: extração, classificação, um resumo tipado. Use o function calling quando for o modelo a decidir se executa algo. Ative o strict mode também na definição do tool, para que uma chamada malformada falhe na API em vez de dentro do seu código.

Se tudo que você precisa é um rótulo de um enum curto, compare também com um modelo system one. Quando a saída é restrita a um conjunto conhecido de respostas, um modelo pode escolher uma em vez de gerá-la, e as trocas de latência e custo diferem o bastante para você medir no seu próprio tráfego.

Para todo o resto, o padrão é um esquema strict mais um validador no resultado parseado. O esquema mantém a forma correta. Verificar se o conteúdo está correto continua sendo seu trabalho.

Artigos relacionados