12k
All articles

JSON Schema para Saídas Estruturadas de LLMs

Structured outputs com JSON Schema: como OpenAI, Gemini e Claude impõem um esquema válido, com configuração, validação e armadilhas.

OpenReplay Team
OpenReplay Team
JSON Schema para Saídas Estruturadas de LLMs

Saídas estruturadas restringem a resposta de um LLM a um JSON Schema que você fornece, de modo que o modelo retorne um JSON legível por máquina que corresponde aos seus campos, tipos e enums, em vez de texto livre que você precisa analisar manualmente.

Quem já colocou uma funcionalidade baseada em LLM em produção conhece a alternativa: uma regex para remover blocos markdown perdidos, um try/catch em volta de JSON.parse e um loop de retentativas que dispara com mais frequência do que você gostaria. Funciona bem até a manhã em que, silenciosamente, deixa de funcionar.

O provedor impõe o schema durante a geração, o que transforma “torcer para que o modelo retorne um JSON válido” em um contrato. Isso já funciona hoje na OpenAI, no Google Gemini e no Anthropic Claude. Os três aceitam JSON Schema, portanto o mesmo schema é portável e você altera apenas a montagem da requisição, não o contrato. Este artigo cobre o que são saídas estruturadas, como o JSON Schema as orienta, como a imposição funciona internamente, como configurá-la em cada provedor e os modos de falha contra os quais vale a pena se proteger.

Principais Conclusões

  • O modo JSON garante apenas um JSON sintaticamente válido; saídas estruturadas estritas garantem um JSON que corresponde ao seu schema específico: a diferença entre “faz o parse” e “tem os campos de que você precisa”.
  • A imposição acontece por meio de decodificação restrita (constrained decoding): a cada token, o modelo só pode emitir continuações que mantenham a saída válida em relação ao seu schema, de modo que a conformidade é imposta durante a geração, e não verificada depois.
  • OpenAI, Gemini e Claude falam JSON Schema, portanto um único schema é portável entre provedores; Pydantic e Zod compilam para JSON Schema, que é o fluxo de trabalho que a maioria das equipes realmente usa.
  • Mesmo com o modo estrito ativado, uma recusa ou uma resposta truncada por limite de tamanho retorna com sucesso, mas não é um JSON válido segundo o schema, então valide o objeto após o parse antes de confiar nele.
  • Cada provedor suporta apenas um subconjunto do JSON Schema, portanto palavras-chave como minimum, pattern ou recursão profunda podem ser descartadas ou rejeitadas. Verifique a documentação do subconjunto suportado por cada provedor.

De texto livre a JSON imposto por schema

Deixados por conta própria, LLMs emitem texto livre que quebra parsers: adicionam prosa em volta do JSON, omitem aspas ou inventam campos. Saídas estruturadas resolvem isso restringindo a resposta a um JSON Schema, de modo que a saída seja legível por máquina e passível de parse de forma confiável.

Esta é a evolução em relação ao antigo modo JSON. A OpenAI introduziu o modo JSON em 2023 como uma forma de forçar JSON válido, mas ele só promete que a saída fará o parse, não que seguirá qualquer schema que você defina. As saídas estruturadas estritas fecham essa lacuna ao impor o próprio schema. Na avaliação da própria OpenAI sobre aderência a schemas complexos, o modelo gpt-4o-2024-08-06 com Structured Outputs alcançou 100%, contra menos de 40% do antigo gpt-4-0613. Trata-se de um benchmark específico de modelo, não de uma garantia universal, mas ilustra a mudança de “geralmente faz o parse” para “corresponde ao schema”.

Como o JSON Schema se encaixa nisso?

Um JSON Schema é um contrato declarativo para seus dados: ele declara tipos, campos obrigatórios, enums e restrições de valor. Você o envia junto com a requisição, e o provedor restringe a geração para corresponder a ele. Um schema compacto para extração de contatos se parece com isto:

{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "email": { "type": "string" },
    "plan_interest": {
      "type": "string",
      "enum": ["starter", "pro", "enterprise"]
    }
  },
  "required": ["name", "email", "plan_interest"],
  "additionalProperties": false
}

Poucas pessoas escrevem isso à mão em produção. O fluxo de trabalho comum é definir o formato em Pydantic (Python) ou Zod (TypeScript) e deixar o SDK emitir o JSON Schema. Os SDKs da OpenAI suportam isso diretamente: entregue a eles um objeto Pydantic ou Zod e eles geram o JSON schema correspondente, convertem a resposta de volta no seu objeto tipado e expõem as recusas para você. O Gemini adicionou a mesma conveniência, estendendo o suporte a JSON Schema a todos os modelos Gemini ativamente suportados, de modo que schemas Pydantic e Zod funcionam sem uma etapa de conversão.

Se você preferir não montar manualmente o schema nem o wrapper específico de cada provedor, o JSON Schema builder do OpenReplay faz as duas coisas no navegador. Você adiciona campos com tipos, descrições, enums e aninhamento, ou cola um JSON de exemplo para inferir um ponto de partida, e então copia o resultado no painel de exportação, que oferece JSON Schema, response_format da OpenAI, ferramenta de função da OpenAI, Anthropic, Gemini, Zod e Pydantic. Nada do que você digita sai da página.

Como funciona a imposição do schema?

Saídas estruturadas funcionam porque o provedor restringe a decodificação: a cada passo da geração, o modelo só pode produzir tokens que mantenham a saída válida em relação ao seu schema. O schema é compilado em uma gramática (ou em uma máquina de estados finitos), e os tokens que a violariam são mascarados antes da amostragem. A própria documentação da Anthropic descreve o mesmo mecanismo: seu schema é compilado em uma gramática, e a amostragem restrita mantém a geração dentro dela, de modo que o modelo não tem como emitir um token que quebre o schema.

A mesma ideia se generaliza para além de JSON. Para modelos locais e auto-hospedados, o llama.cpp usa arquivos de gramática GBNF e o Outlines aplica restrições baseadas em regex e gramáticas, ambos impondo formatos arbitrários (SQL, DSLs personalizadas ou JSON) pelo mesmo princípio de mascaramento de tokens.

Saídas estruturadas nos diferentes provedores

Os três principais provedores falam JSON Schema, então o padrão é portável. O que difere é a montagem da requisição e os sinais de falha.

ProvedorOnde vai o schemaDialeto do schemaSinal de recusa / incompletude
OpenAIresponse_format (Chat Completions) ou text.format (Responses API), com strict: trueSubconjunto do JSON Schemacampo refusal; finish_reason: "length"
GeminiresponseFormat.text (mimeType + schema) em generationConfigSubconjunto do JSON Schema (incl. anyOf, $ref)candidato truncado; rejeição de schemas excessivamente complexos
Claudeoutput_config.format, ou strict: true no input_schema de uma ferramentaSubconjunto do JSON Schemastop_reason: "refusal" / "max_tokens"

OpenAI. Defina strict: true e passe o schema. O guia da OpenAI recomenda iniciar novos projetos com seus modelos atuais e observa que a Responses API mudou o parâmetro: use response_format: { type: "json_schema", strict: true } no Chat Completions, ou text: { format: { type: "json_schema", strict: true } } na Responses API.

Gemini. Forneça o schema por meio de generationConfig:

response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="Extract the contact from this email...",
    config={
        "response_format": {
            "text": {
                "mime_type": "application/json",
                "schema": person_schema,
            }
        },
    },
)

Claude. A Anthropic disponibiliza saídas estruturadas nativas, não mais um contorno via chamada de ferramenta. São dois recursos complementares: saídas JSON via output_config.format para o corpo da resposta, e uso estrito de ferramentas via strict: true para as entradas das ferramentas, utilizáveis de forma independente ou em conjunto. O uso estrito de ferramentas garante que os argumentos de uma chamada correspondam ao seu input_schema, porque esse schema é compilado em uma gramática que restringe a amostragem — a mesma família de técnicas usada por OpenAI e Gemini. Note que a superfície da API mudou na versão GA: o parâmetro output_format passou para output_config.format, e os cabeçalhos beta não são mais necessários.

Armadilhas e boas práticas

O modo estrito não é uma garantia de saída passível de parse, e o suporte a schemas não é universal. Proteja-se contra estes pontos.

Mantenha os schemas planos. Estruturas profundamente aninhadas ou recursivas são a causa mais comum de erros de “schema muito complexo” e de degradação do raciocínio. O Claude sinaliza isso diretamente, retornando 400 quando a gramática compilada cresce demais, e a documentação do Gemini alerta que schemas muito grandes ou profundamente aninhados podem ser rejeitados. Nomes de propriedades longos, arrays grandes, enums com muitos valores e objetos cheios de propriedades opcionais aumentam o custo. Divida extrações grandes em schemas menores e mais planos.

Trate recusas e truncamentos. Mesmo com o modo estrito ativado, uma recusa ou uma resposta truncada por limite de tamanho retorna um status de sucesso, mas não é um JSON válido segundo o schema. A OpenAI adicionou um sinal dedicado para isso: um campo refusal na resposta informa que o modelo recusou, em vez de retornar algo que corresponda ao seu schema. Não existe um limite universal de tokens de saída. Os limites variam por modelo, e qualquer resposta que atinja o teto no meio de um objeto produz JSON inválido, então dimensione seu max_tokens para o pior caso e verifique o teto documentado do seu modelo.

Verifique o subconjunto suportado. Cada provedor suporta apenas uma fatia do JSON Schema no modo estrito. O guia da OpenAI é explícito ao dizer que boa parte da especificação é coberta, mas algumas partes ficam de fora, seja por desempenho, seja por razões técnicas. Palavras-chave como minimum, pattern ou valores padrão podem ser descartadas ou rejeitadas, então consulte a documentação do subconjunto suportado pelo provedor em vez de presumir a especificação completa.

Valide mesmo assim. Como recusas e truncamentos produzem respostas com status válido, mas JSON inválido, faça o parse e valide o objeto contra o seu schema antes de confiar nele, mesmo com strict: true.

Raciocine primeiro, depois emita. Restringir a saída pode reduzir a qualidade do raciocínio em algumas tarefas. Um guia prático sobre as saídas estruturadas do Claude trata isso como um trade-off real em relação ao extended thinking: se uma tarefa ganha mais com o raciocínio do modelo do que com a conformidade garantida ao schema, deixe o raciocínio sem restrições. Um caminho intermediário prático é permitir que o modelo raciocine em uma fase de pensamento e, então, restringir apenas o JSON final.

Saídas estruturadas transformam respostas de LLM em algo que você pode tratar como uma API tipada, e o padrão se transfere de forma limpa entre OpenAI, Gemini e Claude, porque todos aceitam JSON Schema. Comece definindo seu formato em Pydantic ou Zod, ative o modo estrito no seu provedor, mantenha o schema plano e envolva o parse em uma validação que trate recusas e truncamentos; depois, conecte o mesmo schema a qualquer provedor com o qual você for para produção.

Perguntas Frequentes

Qual é a diferença entre o modo JSON e as saídas estruturadas estritas?

O modo JSON garante apenas que o modelo retorne um JSON sintaticamente válido, que faz o parse sem erros, mas não garante que a saída corresponda a qualquer schema específico. As saídas estruturadas estritas impõem o seu JSON Schema específico durante a geração, de modo que o objeto retornado tenha os campos, tipos e enums que você definiu. A distinção é 'faz o parse' versus 'tem os campos de que você precisa'. A OpenAI introduziu o modo JSON em 2023 e, posteriormente, adicionou saídas estruturadas impostas por schema para fechar essa lacuna.

Ativar o modo estrito garante que eu sempre receba um JSON válido e passível de parse?

Não. O modo estrito restringe a geração de tokens ao seu schema durante uma conclusão normal, mas uma recusa por segurança ou uma resposta truncada por limite de tamanho ainda retorna status de sucesso enquanto produz uma saída que não é um JSON válido segundo o schema. A OpenAI expõe um campo refusal dedicado e um finish_reason igual a length; o Claude sinaliza esses casos com um stop_reason igual a refusal ou max_tokens. Como essas respostas retornam 200 e são cobradas, você deve fazer o parse e validar o objeto contra o seu schema antes de confiar nele.

Posso reutilizar o mesmo JSON Schema em OpenAI, Gemini e Claude?

Em grande medida, sim. OpenAI, Google Gemini e Anthropic Claude aceitam JSON Schema, portanto o mesmo schema é portável e você altera apenas a montagem da requisição, não o contrato. O que difere é onde o schema vai: a OpenAI usa response_format ou text.format com strict true, o Gemini aninha o schema em responseFormat.text dentro de generationConfig, e o Claude usa output_config.format ou uso estrito de ferramentas. Cada provedor suporta apenas um subconjunto do JSON Schema, então verifique as palavras-chave não suportadas na documentação do subconjunto suportado por cada provedor antes de presumir portabilidade total.

Por que meu schema é rejeitado como muito complexo e como corrijo isso?

Os limites de complexidade vêm de estruturas profundamente aninhadas ou recursivas, nomes de propriedades longos, limites grandes de arrays, enums com muitos valores ou objetos com muitas propriedades opcionais. O Claude retorna 400 quando a gramática compilada cresce demais, e o Gemini pode rejeitar schemas muito grandes ou profundamente aninhados. A correção é manter os schemas planos e dividir extrações grandes em schemas menores e mais planos. Estruturas profundamente aninhadas também são uma causa comum de degradação do raciocínio, então achatá-las melhora tanto a aceitação quanto a qualidade da saída.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.