Os dez erros de JSON que realmente quebram um payload
Em ordem de frequência, não de interesse, com a mensagem de erro exata que cada um produz.
Cada afirmação desta página foi medida ou tem fonte. Quando não é nem uma coisa nem outra, a página diz isso.
Quase todo chamado de “JSON inválido” é uma de dez coisas. O parser te dá uma posição, às vezes um caractere, e nunca a causa. Aqui estão eles em ordem aproximada de frequência, com a mensagem que você terá visto e a correção.
1. Vírgulas finais
O mais comum de longe, porque todo outro formato que você escreve o dia inteiro permite.
{ "a": 1, "b": 2, }
O V8 te dá uma mensagem que não menciona nem vírgulas nem o que você fez:
Expected double-quoted property name in JSON at position 18 (line 1 column 19)
O parser consumiu a vírgula, esperou outra chave e esbarrou em }. Agora faça a mesma coisa num array:
Unexpected token ']', "[1,2,]" is not valid JSON
Redação completamente diferente para o erro idêntico, porque no parser o caminho dos arrays falha em outra produção da gramática. Se você está pesquisando a string do erro para entender o que aconteceu, essa assimetria é o motivo de você não achar nada útil.
O Python é mais direto, mas só recentemente. No 3.13 e posteriores:
Illegal trailing comma before end of object
No 3.12 e anteriores a mesma entrada dá Expecting property name enclosed in double quotes: line 1 column 19 (char 18). O mesmo interpretador, o mesmo defeito no seu arquivo, duas explicações diferentes conforme a versão que a sua CI por acaso fixou.
Correção: apague a vírgula. { "a": 1, "b": 2 }.
2. Aspas simples
Um dict de Python que passou por print() ou str() em vez de json.dumps():
{'ok': True}
Isso não é JSON e nunca foi. Falha na primeira aspa:
Expected property name or '}' in JSON at position 1 (line 1 column 2)
Repare também no True, que é uma segunda falha, separada, esperando atrás da primeira. Os booleanos de JSON são em minúsculas.
{"ok": true}
Corrija na origem: json.dumps(obj), e se a saída vai para um arquivo UTF-8 ou um corpo HTTP, json.dumps(obj, ensure_ascii=False) para que os caracteres acentuados continuem legíveis em vez de virarem escapes \uXXXX. Mais separators=(",", ":") se você quiser compacto.
3. Chaves sem aspas
Um literal de objeto JavaScript colado direto num campo JSON:
{ name: "ada", active: true }
Expected property name or '}' in JSON at position 2 (line 1 column 3)
O JSON exige que toda chave seja uma string entre aspas duplas. Não aspas simples, não sem aspas, não um número. {"name": "ada", "active": true}. É a mesma classe de colagem do item anterior e a correção é a mesma: tire o valor do runtime com um serializador de verdade em vez de tirar de um log de console.
4. Caracteres de controle sem escape
Uma quebra de linha real dentro de um literal de string:
{"note": "line one
line two"}
Bad control character in string literal in JSON at position 18 (line 1 column 19)
O Python chama de Invalid control character at: line 1 column 19 (char 18). De um jeito ou de outro o parser está te dizendo que apareceu um caractere abaixo de U+0020 dentro de uma string, onde só o escape dele é permitido.
{"note": "line one\nline two"}
Tabulações são o mesmo problema e mais difíceis de ver, porque uma tabulação colada num valor parece espaços. A causa raiz é quase sempre JSON montado com concatenação de strings, em que um campo contendo uma quebra de linha entra literalmente. A última seção trata disso direito.
5. Caminhos do Windows
{"path": "C:\Users\ada\config.json"}
\U e \a não são escapes válidos. O Python é explícito: Invalid \escape: line 1 column 13 (char 12). O V8 diz Bad escaped character in JSON at position 13 (line 1 column 14), apontando para o U em vez da barra invertida. Os nove escapes legais são \" \\ \/ \b \f \n \r \t e \uXXXX. Todo o resto é erro, o que é o design correto e surpreende o tempo todo.
{"path": "C:\\Users\\ada\\config.json"}
Barras normais funcionam bem no Windows em quase toda API, e não custam barras invertidas dobradas. Se você tem um bloco de texto para embutir e prefere não fazer isso à mão, a ferramenta de escape faz, e o unescape faz o caminho inverso.
6. Caracteres invisíveis
Este é o que come uma tarde inteira. Duas variantes:
Espaço não separável (U+00A0). Copie um trecho de uma página de documentação, de um cliente de chat ou de um PDF e os espaços entre os tokens podem não ser espaços. A RFC 8259 permite exatamente quatro caracteres de espaço em branco entre tokens: espaço, tabulação, retorno de carro e quebra de linha. U+00A0 não é um deles, então é erro de sintaxe, e ele aparece idêntico ao caractere ao lado.
Aspas tipográficas. O Word e o Google Docs autocorrigem a aspa reta U+0022 para o par tipográfico U+201C e U+201D enquanto você digita. O JSON aceita só U+0022. Um documento que parece perfeitamente entre aspas na tela não tem delimitador de string nenhum.
Nenhuma das duas variantes produz uma mensagem que nomeie o codepoint. Dependendo de onde o caractere cai você recebe Expected double-quoted property name in JSON at position 8, ou um Unexpected token ' ' que imprime de volta um caractere que você não distingue de um espaço normal. Cole o documento no validador e ele nomeia o caractere e o codepoint no deslocamento exato, que é a forma mais rápida de achar. O Reparar JSON remove esses caracteres e te diz o que tirou.
7. Comentários
{
// o nome de exibição do usuário
"name": "ada"
}
O V8 relata Expected property name or '}' in JSON at position 4 (line 2 column 3). O Python para com Expecting property name enclosed in double quotes: line 2 column 3 (char 4). Os dois apontam para a barra, e nenhum diz a palavra comentário, então a mensagem se lê como um problema de aspas numa linha que não contém string alguma.
JSON não tem sintaxe de comentário. Crockford removeu de propósito, porque as pessoas usavam comentários para carregar diretivas de parsing. Se você controla o consumidor, JSONC (o que o VS Code usa nas próprias configurações) permite comentários e vírgulas finais, e JSON5 permite bem mais. Se não controla, mova a prosa para um campo, ou para o schema, que é onde descrições pertencem. O argumento completo vale dez minutos se você está escolhendo um formato de configuração.
8. NaN e Infinity
{"ratio": NaN}
Unexpected token 'N', "{"ratio": NaN}" is not valid JSON
A armadilha é que o Python emite isso por padrão. json.dumps({"ratio": float("nan")}) produz {"ratio": NaN} e não levanta nada, porque o encoder do CPython é deliberadamente tolerante e o decoder dele aceita o valor de volta. Todo consumidor que não seja Python rejeita.
json.dumps(obj, allow_nan=False) # levanta ValueError em vez de enviar JSON inválido
Ligue isso hoje na sua camada de serialização. Um NaN que chega em produção é uma divisão que você não protegeu, e você prefere encontrá-la no encoder do que no parser de um cliente.
9. Chaves duplicadas
{"id": 1, "id": 2}
Nenhum erro. A RFC 8259 diz que as chaves DEVERIAM ser únicas e deixa o comportamento indefinido quando não são. JavaScript e Python ficam ambos com a última, então isso faz parsing como {"id": 2} e o seu primeiro valor sumiu sem deixar rastro. Outros parsers ficam com a primeira, e alguns levantam erro. É o único item da lista que é silencioso, o que o torna o pior. Passe um payload pelo validador, que sinaliza duplicatas em vez de colapsá-las em silêncio.
10. Números
Duas falhas dividem esta vaga.
Zeros à esquerda. {"code": 007} é inválido. A gramática do JSON permite um único 0, ou um dígito de 1 a 9 seguido de mais dígitos, e nada além disso. Um CEP, um código de país ou um número de peça com zero à esquerda é uma string. {"code": "007"}.
Inteiros acima de 2^53-1. {"id": 12345678901234567890} faz parsing sem problema e volta como outro número, porque o JavaScript guarda como double IEEE 754 e o Number.MAX_SAFE_INTEGER é 9007199254740991. Sem erro, sem aviso, registro errado. Envie IDs grandes como strings; a versão longa explica por que qualquer outra correção é remendo.
A correção estrutural
Metade desta lista (os itens 2, 3, 4 e 5) vem do mesmo hábito: produzir JSON com outra coisa que não um serializador, geralmente concatenação de strings ou um log de console.
# cada um destes é um bug esperando a entrada certa
body = '{"note": "' + note + '", "path": "' + path + '"}'
Uma quebra de linha em note quebra. Uma barra invertida em path quebra. Uma aspa em qualquer um dos dois quebra, e se aquela entrada veio de um usuário isso é injeção, não problema de formatação.
body = json.dumps({"note": note, "path": path}, allow_nan=False)
O serializador escapa o que precisa de escape, coloca aspas onde precisa, e rejeita o que não pode ser representado. Não é preferência de estilo. Montar JSON à mão significa reimplementar corretamente as regras de escape da seção 7 da RFC 8259 em cada ramo, e ninguém faz isso.
Quando o que te entregam é um documento quebrado e não um produtor quebrado, o Reparar JSON aplica as correções acima e imprime a lista de cada mudança que fez, para você ver se ele chutou alguma coisa antes de confiar na saída.