Pular para o conteúdo
mnzes

Como garantir que o modelo devolva JSON válido?

PorDiógenes MenezesAprendendo IA em público

13 min de leitura

Você tem quatro degraus, do mais barato ao mais garantido: pedir o formato no prompt, ligar o JSON mode do provedor, entregar um schema estrito ou compilar sua própria gramática. Só os dois últimos garantem alguma coisa sobre os campos. Nenhum deles impede truncamento por limite de tokens, então o validador continua necessário mesmo no topo da escada.

Este artigo é a parte prática de saídas estruturadas: qual botão apertar, o que cada um deixa de fora e como consertar o que sobra. A parte de declarar o formato e conferir os valores está em JSON Schema e Zod.

O quarto degrau, escrever a gramática você mesmo, tem artigo próprio em grammar-constrained decoding. Aqui ele aparece só como o teto da escada. Vale lembrar antes de começar que a instrução de formato vive na mensagem de sistema, e que os três papéis não são intercambiáveis para isso: instrução de formato repetida na mensagem do usuário compete com a entrada real em vez de reforçar a regra.

Onde o JSON quebra

cerca de markdown```jsonvírgula final"qtd": 3, }aspas não escapadas"obs": "tipo "1""truncamento"total": 148.comentário// opcional
Figura 1Cinco falhas, cinco consertos diferentes. Quatro são de uma linha de código; a quinta, o truncamento, é a única que nenhum conserto de texto resolve.

Antes de subir degrau, vale saber o que você está tentando evitar. Cada falha tem uma causa diferente e um conserto diferente, e três delas são resolvidas sem mexer no prompt.

A cerca de markdown é a mais comum. O modelo devolve o objeto certo dentro de ```json, às vezes com uma frase de cortesia antes. A causa é estatística: ele viu muito mais JSON dentro de bloco de código do que solto. Conserto: recorte entre a primeira chave e a última.

A vírgula final vem de um , sobrando antes de } ou de ]. Aparece quando a lista de campos é longa e o modelo perde a conta do último item. Conserto: remover com uma substituição antes do parse, ou subir para o degrau 2.

As aspas aparecem de duas formas que só têm o nome em comum. Aspas tipográficas no lugar das retas acontecem quando o conteúdo do campo veio de texto formatado; aspas retas não escapadas dentro de uma string fecham o valor no meio e desalinham tudo depois. A segunda é a pior, porque o parser falha em uma posição que não tem relação com a causa.

O truncamento é a única que nenhum tratamento de texto resolve. O JSON está correto até onde foi, e acabou porque bateu no limite de tokens. O sintoma é falhar só nos casos longos, o que parece aleatório até você olhar o motivo de parada da resposta e encontrar o limite em vez do fim natural.

Os comentários são a mais rara e a mais fácil de diagnosticar errado. O modelo anota // opcional ao lado de um campo, geralmente quando o seu prompt descreveu o schema em prosa com anotações desse tipo. JSON não tem comentário, e o parser recusa. Conserto real: tirar os comentários do exemplo que você mandou.

A escada

pedir no promptgarante: nadaJSON modegarante: sintaxeschema estritogarante: sintaxe e camposgramática própriagarante: qualquer formato
Figura 2Cada degrau garante uma camada a mais e fecha uma classe de erro. Nenhum deles garante que o valor dentro do campo está certo.

Os quatro degraus não são alternativas concorrentes; são camadas, e cada uma fecha uma classe de erro que a anterior deixava aberta. O erro de projeto mais comum é pular direto para o topo por precaução, pagar a complexidade e continuar sem validador — porque nenhum degrau resolve o problema que interessa, que é o valor dentro do campo estar certo.

Degrau 1: pedir no prompt

Barato, imediato e o suficiente para descobrir se a tarefa funciona. Três coisas mudam a taxa de acerto de verdade, e nenhuma delas é escrever a instrução em maiúsculas.

Mostre a saída, não descreva. Um exemplo resolvido comunica nome de campo, tipo, formato de data e profundidade de aninhamento em um só golpe. Descrever isso em prosa gasta mais tokens e comunica menos.

Diga o que não deve aparecer. “Responda apenas com o objeto JSON, sem cerca de código e sem texto antes ou depois” corta a maior parte das cercas. É uma das poucas instruções negativas que rendem aqui.

Deixe folga no limite de tokens. Dimensione pelo pior caso, não pelo caso médio. Uma lista de itens que costuma vir com três elementos e um dia vem com quarenta é o cenário clássico de truncamento.

E um detalhe de arquitetura que muda mais do que parece: se o schema é fixo, ele pertence à mensagem de sistema, que é a parte estável do prompt. Isso deixa a mensagem do usuário com a entrada real e mantém o prefixo idêntico entre chamadas, o que é pré-requisito para o cache de prompt funcionar.

O prefill, onde ele existe

user: extraia o pedidoassistant: {"cliente": "Zé", ...}última mensagem,sem fechara continuação já começadentro do objetoo que você envia
Figura 3Com a chave já escrita, o primeiro token que o modelo gera é conteúdo do objeto. A cerca de markdown fica inalcançável em vez de improvável.

O prefill é o truque mais barato do degrau 1. Em vez de terminar a lista de mensagens com o usuário, você acrescenta uma mensagem de assistant contendo apenas { e manda assim. O modelo continua de onde a mensagem parou, e ele não pode mais abrir uma cerca de markdown, porque a cerca teria que vir antes da chave que já está escrita.

Duas armadilhas. A primeira: a chave que você escreveu não volta na resposta. Você recebe a continuação, e é o seu código que precisa colar o { de volta antes de fazer o parse. A segunda: nem toda API aceita. Vários provedores passaram a exigir que a última mensagem seja do usuário e devolvem erro 400 no caso contrário — em julho de 2026 isso já vale para parte dos modelos comerciais mais recentes, enquanto os modelos de peso aberto servidos localmente continuam aceitando sem restrição. Onde o prefill não existe, o substituto não é insistir no prompt, é subir a escada.

Degrau 2: JSON mode

JSON mode é um parâmetro na chamada que restringe a decodificação a produzir um JSON sintaticamente válido. Não é uma instrução mais enfática nem um pós- processamento: a partir dele, os tokens que levariam a uma sequência impossível recebem probabilidade zero antes da amostragem.

O que ele garante é exatamente uma coisa: o texto abre no parser. As quatro falhas de sintaxe da figura 1 desaparecem, inclusive o comentário e a vírgula final, porque nenhuma delas é JSON legal.

O que ele não garante é tudo o resto. A estrutura é livre, então {"resposta": "não sei"} satisfaz o JSON mode do mesmo jeito que o objeto que você esperava. Campo faltando, nome de campo inventado, tipo trocado e enum fora da lista continuam todos possíveis. E o truncamento continua possível, porque o limite de tokens é anterior à restrição.

Uma consequência que pega gente desprevenida: com JSON mode ligado e um prompt que não diz claramente o que produzir, o modelo às vezes emite um objeto vazio ou entra em uma sequência longa de espaços em branco até bater no limite. A restrição garante o formato e não substitui a instrução.

Degrau 3: schema estrito

Aqui você entrega o JSON Schema junto com a chamada e o provedor o compila em uma restrição sobre a decodificação. A diferença para o degrau 2 é que a restrição passa a conhecer os seus campos: depois de {" só passam os prefixos de nome de campo que existem no schema, depois de "qtd": só passam dígitos, e um enum de três valores torna o quarto valor inalcançável.

A objeção óbvia é o custo de testar o vocabulário inteiro a cada token. Willard e Louf resolveram isso construindo, antes da geração, um índice que mapeia cada estado do autômato para o conjunto de tokens aceitáveis ali, o que torna o trabalho por token constante em média1. O XGrammar levou a ideia adiante ao separar os tokens que podem ser pré-verificados uma vez daqueles que dependem do contexto da pilha, e ao sobrepor o cálculo da gramática à execução na GPU: os autores reportam até 100 vezes mais velocidade que soluções anteriores e sobrecarga próxima de zero na geração ponta a ponta2. É por isso que schema estrito deixou de ser uma escolha de desempenho.

Três detalhes operacionais que aparecem em produção:

  1. O schema precisa ser fechado. Os provedores costumam exigir additionalProperties: false e a lista explícita de campos obrigatórios. Um schema que aceita campo extra não vira uma restrição útil.
  2. A primeira chamada com um schema novo é mais lenta. A compilação acontece uma vez e fica em cache do lado do provedor. Montar o schema dinamicamente a cada chamada, com a ordem das chaves variando, joga esse cache fora.
  3. A cobertura da especificação é parcial. Um benchmark de 2025 avaliou seis frameworks de decodificação restrita sobre 10 mil schemas reais e encontrou diferenças relevantes de cobertura entre eles3. Restrição numérica, tamanho mínimo de string e schema recursivo são os primeiros a ficar de fora. Na prática, o schema é aceito, a saída “obedece” e a sua regra não foi verificada por ninguém.

Degrau 4: sua própria gramática

O último degrau é usar diretamente o mecanismo que os outros dois embrulham: você escreve a gramática, e o motor de inferência a aplica sobre os logits. É o que a GBNF do llama.cpp e as bibliotecas de geração guiada expõem quando você serve o modelo.

Faz sentido em três situações: quando o formato não é JSON, quando a regra é sensível ao contexto de um jeito que JSON Schema não expressa, ou quando você precisa de garantia sobre um formato que o provedor não oferece. Fora disso, o degrau 3 entrega o mesmo com menos código para manter.

O recorte que resolve a maioria dos casos

Se você ficou no degrau 1, este é o único código que importa. Ele resolve cerca de markdown, prosa antes e prosa depois de uma vez, sem tratar cada caso:

function extrair(bruto: string) {
  const i = bruto.indexOf('{');
  const j = bruto.lastIndexOf('}');
  if (i < 0 || j < i) throw new Error('nenhum objeto na resposta');
  return JSON.parse(bruto.slice(i, j + 1));
}

Antes do recorte, cheque o motivo de parada. Se a resposta terminou por limite de tokens, o objeto está incompleto e nenhum conserto de texto vai salvá-lo — o que você quer ali é repetir com um limite maior, não tentar fechar as chaves à mão.

Quando o parse falha mesmo assim, devolva o erro do validador ao modelo junto com a resposta anterior, em vez de reenviar o mesmo prompt. Uma passada de conserto assim é barata e mensurável: no estudo de Tam e colegas, usar um modelo pequeno para reformatar saídas com erro de parse melhorou o resultado final em JSON e em YAML4.

Onde isso falha

Sintaxe válida não é resposta correta. É a confusão mais cara do assunto. Com schema estrito, {"categoria": "cobranca"} abre no parser e o valor está no enum; nada disso diz que aquele ticket era de cobrança. Subir a escada troca uma falha barulhenta por uma falha silenciosa, e sem avaliação de conteúdo o segundo tipo passa despercebido por muito mais tempo.

A ordem dos campos muda o resultado. O modelo escreve na ordem em que os campos aparecem no schema. Se o campo de resultado vem antes do campo de raciocínio, não existe raciocínio: a resposta já foi escrita quando a justificativa começa. Tam e colegas encontraram esse padrão de forma consistente em modo JSON, e o conserto é declarar o raciocínio primeiro4.

Restrição mal implementada custa acurácia. Beurer-Kellner e colegas mediram uma versão de GSM8K codificada em JSON com Mistral 7B e cinco exemplos: 41,5% de acerto sem restrição, 30,8% com uma restrição ingênua e 41,8% com uma restrição alinhada a subpalavras5. A causa é a máscara forçar uma fragmentação de tokens que o modelo nunca viu no treino. Vale como aviso de que a biblioteca importa tanto quanto a decisão de restringir.

O parser raramente é o culpado quando a qualidade cai. No mesmo trabalho de Tam e colegas, um modelo teve 0,148% de erro de parse em uma tarefa e mesmo assim 38,15% de diferença de desempenho em relação à saída em texto livre4. Se estruturar piorou o seu resultado, procure na ordem dos campos e no espaço que o formato tirou do raciocínio, não na taxa de falha do parse.

Custo

Ordens de grandeza, para dimensionar a decisão.

Ficar no degrau 1 com conserto custa uma chamada extra na fração que falha. Com 2% de falha, o acréscimo fica em torno de 2% do custo de saída, o que é ruído. Com 30% de falha, você está pagando um terço a mais para automatizar um problema que o prompt resolveria.

Subir para schema estrito custa quase nada em tempo de geração nas implementações atuais12. O custo real é a compilação da primeira chamada de cada schema novo e a disciplina de manter os schemas estáveis para o cache servir.

Por onde começar

  1. Rode 100 entradas reais no degrau 1 e conte quantas abriram no parser. Sem esse número você está escolhendo degrau por intuição.
  2. Classifique as falhas pela figura 1. Se a maioria é cerca de markdown, o recorte resolve hoje. Se a maioria é truncamento, o problema é limite de tokens e nenhum degrau ajuda.
  3. Ligue o schema estrito quando o custo de um dado errado for alto, não quando a taxa de falha for alta. Ingestão em lote sem supervisão justifica; uma tela com alguém olhando raramente justifica.
  4. Declare o raciocínio antes do resultado em qualquer tarefa que exija pensar.
  5. Mantenha o validador no topo da escada. Ele deixa de ser o mecanismo principal e vira a rede contra truncamento e contra o pedaço do schema que a restrição não cobre.

Footnotes

  1. Willard e Louf (2023) reformulam a geração guiada como transições entre estados de um autômato finito e pré-computam um índice sobre o vocabulário do modelo, o que torna o custo por token constante em média. 2

  2. Dong et al. (2024) dividem o vocabulário entre tokens independentes de contexto, verificáveis uma vez, e tokens dependentes da pilha, e sobrepõem o cálculo da gramática à execução na GPU; reportam até 100 vezes mais velocidade que soluções anteriores. 2

  3. Geng et al. (2025) avaliam seis frameworks de decodificação restrita sobre 10 mil schemas reais de JSON Schema, em eficiência, cobertura e qualidade.

  4. Tam et al. (2024) comparam decodificação restrita, instrução de formato e conversão em duas etapas, e medem o efeito da ordem dos campos e da passada de reformatação. 2 3

  5. Beurer-Kellner et al. (2024) medem a perda de acurácia causada por desalinhamento entre a máscara e as subpalavras do modelo, e apresentam um algoritmo alinhado que a elimina.

Perguntas frequentes

Por que o LLM devolve JSON quebrado?
Porque ele escolhe um token por vez sem nenhuma noção de que existe uma chave aberta esperando fechamento. Validade de JSON é propriedade da sequência inteira, e a amostragem é local. O modelo acerta o formato por imitação do que viu no treino, não porque algo o impeça de errar.
O que é JSON mode?
É um parâmetro que restringe a decodificação para que a resposta seja um JSON sintaticamente válido, com qualquer estrutura. Ele garante que o texto abre no parser e nada além disso: campo faltando, nome errado e tipo trocado continuam possíveis. Para garantir também os campos você precisa de um schema estrito.
Como remover a cerca de markdown do JSON?
Recorte da primeira chave até a última, com `bruto.slice(bruto.indexOf('{'), bruto.lastIndexOf('}') + 1)`. Isso resolve cerca de código, prosa antes e prosa depois de uma vez, sem regex e sem tratar cada caso. Para array, use os índices de colchete.
JSON mode resolve truncamento?
Não. A restrição impede token inválido, não impede a resposta acabar no meio porque bateu no limite de tokens. O JSON truncado é sintaticamente impecável até onde foi. Confira o motivo de parada da resposta antes de fazer o parse: se for limite de tokens, o dado está incompleto.
Preciso de schema estrito se já uso JSON mode?
Precisa se algum campo é obrigatório para o seu código. JSON mode entrega um objeto qualquer; schema estrito entrega o objeto que você declarou, com os campos, os tipos e os valores de enum. A diferença aparece no dia em que o modelo resolve renomear um campo.
O prefill ainda funciona?
Funciona onde a API aceita uma mensagem de assistant no fim da lista, o que inclui a maioria dos modelos de peso aberto. Vários provedores passaram a recusar isso com erro 400 em julho de 2026, e nesses casos o substituto é a saída estruturada nativa, que garante mais e custa o mesmo.

Referências

  1. Willard, B. T. e Louf, R.. Efficient Guided Generation for Large Language Models (2023)arXiv:2307.09702
  2. Dong, Y. et al.. XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models (2024)arXiv:2411.15100
  3. Tam, Z. R. et al.. Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Models (2024)arXiv:2408.02442
  4. Geng, S. et al.. JSONSchemaBench: A Rigorous Benchmark of Structured Outputs for Language Models (2025)arXiv:2501.10868
  5. Beurer-Kellner, L. et al.. Guiding LLMs The Right Way: Fast, Non-Invasive Constrained Generation (2024)arXiv:2403.06988