Pedir JSON no prompt não é o mesmo que garantir JSON
Escrever "responda em JSON" no prompt não garante JSON válido, e essa é a primeira coisa que todo integrador de IA aprende do jeito difícil. Funciona nos testes, funciona na demo, funciona por semanas — até o dia em que o modelo devolve o objeto embrulhado num "Claro! Aqui está o resultado:" e o JSON.parse estoura em produção, no meio de um fluxo que ninguém estava olhando. O problema não é você ter escrito o prompt errado. É que a instrução textual nunca foi uma garantia, só uma sugestão com alta taxa de sucesso.
Por que a instrução no prompt não basta
Um modelo de linguagem não "monta um objeto". Ele prevê o próximo token, um de cada vez, com base na probabilidade condicionada a tudo que veio antes. Quando você pede JSON, você está empurrando a distribuição na direção de um texto que se parece com JSON — e na maioria das vezes isso é suficiente. Mas "na maioria das vezes" é exatamente o problema.
Alguns modos de falha que aparecem sozinhos, sem você mudar nada:
- Texto de cortesia em volta. O modelo prefixa com "Segue o JSON solicitado:" ou fecha com "Espero que ajude!". Sintaticamente o miolo está certo, mas o
parserecebe a frase inteira e falha. - Cerca de markdown. A resposta vem dentro de um bloco
```json ... ```. Ótimo para renderizar num chat, péssimo para um parser que esperava a primeira chave{no primeiro caractere. - Vírgula sobrando. Um
,antes do}ou do]final. É JSON quase válido — que é a mesma coisa que inválido. - Aspas erradas. Aspas tipográficas em vez de retas, ou aspas simples onde JSON exige duplas.
- Campo inventado. O modelo decide que seria útil incluir uma chave que ninguém pediu, e o consumidor lá na frente não sabe o que fazer com ela.
- Valor fora do combinado. Você pediu
statuscomo"ativo"ou"inativo"e voltou"pendente", ou um número onde você esperava string.
Nada disso é o modelo "desobedecendo". É a cauda de uma distribuição de probabilidade que, na escala de milhares de chamadas por dia, sempre é alcançada. Entrada atípica, prompt longo com instruções competindo entre si, um tópico que puxa o modelo para o modo "assistente conversacional" — qualquer um desses empurra a resposta para fora do formato. Confiar só no prompt é apostar que a cauda nunca vai aparecer. Ela aparece.
O que realmente força o formato
A virada de chave é parar de pedir cooperação e passar a restringir a geração. Vários provedores hoje oferecem algum modo de saída estruturada — os nomes variam entre "structured output", "JSON mode" e "function calling" ou "tool calling", mas a ideia de fundo é a mesma: você entrega uma definição da forma que quer, e a plataforma restringe a decodificação para que a saída obedeça a essa forma.
A diferença conceitual é grande. Uma instrução no prompt é uma preferência: o modelo pode segui-la ou não. Uma restrição no decodificador é uma regra: em cada passo, só os tokens que mantêm a saída válida naquele ponto da estrutura são permitidos. Se o próximo caractere obrigatório é {, nenhum "Claro!" consegue sair antes dele. Se o campo status só admite três valores, é impossível materializar um quarto.
Vale distinguir dois níveis do que esses modos oferecem:
- JSON mode "solto". Garante que a saída é um JSON sintaticamente válido, mas não diz qual. Você elimina a cerca de markdown e o texto de cortesia, mas ainda pode receber campos a mais, campos a menos ou tipos diferentes do que esperava.
- JSON guiado por schema. Você fornece um JSON Schema descrevendo os campos, os tipos, o que é obrigatório e o que é proibido. A saída passa a respeitar a estrutura, não só a sintaxe. É esse o nível que você quer para qualquer coisa que outro sistema vá consumir.
Por isso schema é mais confiável do que instrução em prosa: uma frase como "retorne um campo status com um destes três valores" mora no mesmo espaço de probabilidade que o resto do texto e disputa atenção com ele. Um schema é uma máquina de estados que o decodificador é obrigado a seguir. Um é conselho; o outro é trilho.
Como escrever o schema para ele te ajudar
O schema faz o trabalho pesado, mas só se você for explícito. Alguns hábitos que evitam dor:
- Feche o objeto. Marque os campos obrigatórios e proíba propriedades extras (em JSON Schema,
requiredeadditionalProperties: false). Modelo adora incluir um campo "bônus"; campo bônus quebra consumidor a jusante. - Restrinja os valores quando puder. Se um campo só pode assumir um conjunto fixo de valores, declare esse conjunto (
enum). É a forma mais barata de transformar uma alucinação possível em algo impossível. - Dê uma saída honesta para a incerteza. Se o modelo não achou a informação, ele precisa de um lugar legítimo para dizer isso — um campo anulável, ou algo como
encontrado: false. Sem essa válvula de escape, o caminho de menor resistência é preencher com algo plausível. Boa parte das "alucinações" em tarefas de extração é, na verdade, schema sem escapatória. - Prefira estruturas rasas. Aninhamento profundo e opcional multiplica os caminhos que o modelo pode tomar e as coisas que podem sair diferentes do esperado. Mantenha o objeto tão plano quanto a tarefa permitir.
Um esboço da ideia, no formato do próprio schema:
{
"type": "object",
"properties": {
"encontrado": { "type": "boolean" },
"status": { "type": "string", "enum": ["ativo", "inativo", "pendente"] },
"valor": { "type": ["number", "null"] }
},
"required": ["encontrado", "status", "valor"],
"additionalProperties": false
}
Note que encontrado é obrigatório e valor pode ser null: junto, isso dá ao modelo um jeito de dizer "não achei" sem inventar um número.
Schema garante a forma, não o conteúdo
Aqui vem a parte que a saída estruturada não resolve: o objeto pode estar sintaticamente perfeito, bater com o schema campo por campo, e ainda assim estar errado. Uma data 2026-02-31 é uma string válida e um dia inexistente. Um CPF pode ter onze dígitos e não passar no dígito verificador. Um valor pode estar dentro do tipo number e fora de qualquer faixa que faça sentido no seu domínio. Schema valida gramática, não verdade.
Então a saída do modelo entra na sua aplicação como qualquer outra entrada não confiável, porque é exatamente isso que ela é: um payload vindo de fora do seu controle. Aplique a mesma validação de conteúdo que você aplicaria a um formulário preenchido por um estranho na internet. Datas existem? Identificadores passam no dígito verificador? Valores estão na faixa esperada? Referências apontam para algo que existe no seu banco? Nada disso é responsabilidade do modelo, e nada disso o schema cobre.
Quando ainda assim vier torto: parse e retry
Mesmo com saída estruturada, trate a leitura da resposta como uma operação que pode falhar. O desenho mínimo e resiliente é um laço curto:
- Chame o modelo pedindo a saída estruturada.
- Faça o parse. Se estourar, não deixe a exceção subir crua.
- Valide o conteúdo contra suas regras de domínio, não só contra o schema.
- Se qualquer etapa falhar, tente de novo — de preferência reinjetando no prompt a mensagem de erro concreta ("o campo
valorveio como string, era esperado número"). Modelos costumam se corrigir bem quando o erro é apontado. - Limite as tentativas (duas ou três) e tenha um caminho de fallback: registrar o payload cru, devolver um erro tratado, encaminhar para revisão humana. Retry infinito só troca uma falha barulhenta por uma cara.
Guardar a resposta bruta que falhou vale ouro: é com esses exemplos reais que você descobre se o problema é o schema frouxo, o prompt ambíguo ou um caso de entrada que ninguém tinha previsto.
O ponto prático
Trate a saída do modelo como entrada não confiável num limite de sistema — porque é. Force a estrutura por schema em vez de pedir por gentileza, escreva o schema fechado e com uma saída honesta para a incerteza, valide o conteúdo depois porque a forma certa não garante o valor certo, e embrulhe tudo num parse com retry para os casos que escaparem. Prompt pedindo educadamente é uma sugestão. Schema, validação e um plano de erro são um contrato — e é contrato que segura produção.