JSON não tem comentários, e isso foi de propósito
Os comentários foram tirados do JSON para proteger a interoperabilidade, e cada arquivo de configuração desde então paga essa decisão.
Cada afirmação desta página foi medida ou tem fonte. Quando não é nem uma coisa nem outra, a página diz isso.
Você acrescenta uma linha a um arquivo de configuração explicando por que um timeout é de 45 segundos e não de 30, o deploy quebra, e a mensagem não ajuda em nada:
JSON.parse('{\n // 45s: upstream p99 is 38s\n "timeout": 45\n}')
// Expected property name or '}' in JSON at position 4 (line 2 column 3)
O parser viu uma barra onde deveria haver uma chave e desistiu. O Python não é mais claro sobre a causa:
json.loads('{\n // 45s: upstream p99 is 38s\n "timeout": 45\n}')
# JSONDecodeError: Expecting property name enclosed in double quotes: line 2 column 3 (char 4)
Nenhuma das duas mensagens menciona comentários, porque, no que diz respeito à gramática, não há nada a mencionar. A RFC 8259 define exatamente quatro caracteres que podem aparecer entre tokens: espaço, tabulação, retorno de carro e quebra de linha. Todo o resto ou faz parte de um valor ou é erro de sintaxe.
Por que foram removidos
Os comentários existiam nas primeiras versões do JSON e Douglas Crockford os tirou. O motivo declarado é a parte interessante: as pessoas não os usavam para prosa, usavam para transportar diretivas de parsing. Uma dica de codificação, um ponteiro para um schema, escrito num comentário, que um consumidor específico leria e executaria. A partir daí, o comentário deixa de ser comentário. É um segundo canal de dados, não documentado, viajando dentro de um formato cujo argumento de venda inteiro era que qualquer parser em qualquer lugar leria os mesmos valores a partir dos mesmos bytes.
A saída que o próprio Crockford sugeriu era passar o seu arquivo comentado por um minificador antes de entregá-lo a um parser. Continua sendo a forma certa de resposta, e o resto deste artigo trata basicamente de fazer isso direito.
A decisão era defensável para o que o JSON era nos primeiros anos: um formato de transporte para mover um valor entre dois programas que já haviam concordado sobre o seu significado. Ninguém comenta um pacote de rede.
Por que dói mesmo assim
O JSON não ficou sendo formato de transporte. Virou a linguagem de configuração padrão de toda a cadeia de ferramentas, e configuração é exatamente o caso em que o raciocínio por trás de um valor importa mais do que o valor. Um retries: 0 sem explicação acaba “consertado” pela próxima pessoa, com a melhor das intenções. Um retries: 0 com // proposital, este endpoint não é idempotente em cima, não.
Então cada ecossistema que adotou JSON para configuração inventou o seu próprio remendo por cima, e eles não são compatíveis entre si.
As cinco opções
Uma chave _comment
{
"_comment": "45s porque o p99 do upstream é 38s",
"timeout": 45
}
É JSON estrito, faz parsing em qualquer lugar e não exige ferramenta nenhuma. Os problemas são reais, no entanto. O seu schema agora precisa permitir isso, senão o validador rejeita. É um dado, então vai para os clientes, cai nos logs e aparece nos diffs como mudança de valor em vez de mudança de comentário. E você tem exatamente um por objeto: a RFC 8259 diz que as chaves DEVERIAM ser únicas e deixa duplicatas indefinidas, com JavaScript e Python ambos ficando com a última, de modo que um segundo _comment no mesmo nível engole o primeiro em silêncio. As pessoas contornam com _comment1, _comment2, que é o ponto em que a abordagem deixa de compensar.
Use para uma nota de cabeçalho no topo de um arquivo. Não use para anotar linha a linha.
JSONC
JSONC é JSON mais duas coisas: comentários // e /* */, e vírgulas finais. Só isso. É o que o VS Code usa nos seus próprios settings.json e keybindings.json, e o que o TypeScript aceita em tsconfig.json.
{
// o p99 do upstream é 38s
"timeout": 45,
"retries": 0, // este endpoint não é idempotente
}
Vale ser franco sobre o status: não existe especificação independente de JSONC. Não há RFC, nem número de versão, nem suíte de conformidade. É uma convenção com uma implementação em forma de editor por trás, e os dialetos divergem nas bordas (se uma vírgula final depois do último item de um array é aceita, se comentários sobrevivem a uma ida e volta). É a opção mais segura quando o seu consumidor já é uma ferramenta que suporta, e uma opção ruim para qualquer coisa que você entregue a terceiros.
JSON5
JSON5 é uma especificação de verdade, com histórico de versões, e vai bem mais longe que o JSONC:
- Chaves de objeto sem aspas, quando a chave é um identificador ES5 válido
- Strings com aspas simples
- Vírgulas finais em objetos e arrays
- Comentários de linha e de bloco
- Números hexadecimais
- Pontos decimais no início e no fim, então
.5e5.são números Infinity,-InfinityeNaN
O último item é o que exige pensar direito. NaN e os infinitos não têm representação alguma em JSON, então um documento JSON5 que os use não pode ser convertido para JSON sem uma decisão com perda sobre o que colocar no lugar. As demais extensões são cosméticas e sobrevivem bem a uma conversão. Use JSON5 quando o autor principal do arquivo for uma pessoa e uma extensão .json5 for aceitável; não use como formato de API.
Pare de usar JSON
Se o arquivo é configuração que você controla de ponta a ponta, e nada externo o consome, o formato é escolha livre e o JSON não é obviamente a melhor. YAML e TOML têm comentários de primeira classe. Ambos têm os seus próprios custos, e o comparativo merece uma lida antes de você se comprometer, porque o YAML em particular vai te entregar o problema da Noruega: sob a semântica do YAML 1.1, que PyYAML e o Psych do Ruby implementam, um no sem aspas é lido como o booleano false.
Remova em tempo de build
Mantenha o arquivo anotado como fonte de verdade, remova os comentários na CI, publique JSON estrito. É a sugestão de Crockford e atende a tudo: os seus editores e revisores veem os comentários, o seu parser em tempo de execução vê um documento que satisfaz a RFC 8259, e nenhum consumidor precisa saber que qualquer um dos dois formatos existe.
Remover comentários sem quebrar URLs
A implementação óbvia é uma expressão regular, e a expressão regular óbvia está errada:
// Não faça isso.
text.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
Rode nisto aqui e veja ela destruir um valor:
{
"endpoint": "https://api.example.com/v2/orders", // produção
"note": "consulte /* o runbook */ antes de mudar isso"
}
A primeira regra encontra // dentro de https:// e apaga o resto da linha, incluindo a aspa de fechamento e a vírgula. A segunda encontra um comentário de bloco dentro de uma string. Você termina com uma string não terminada e um erro apontando para um lugar que não tem nada a ver. Uma regex não consegue fazer este trabalho porque não consegue saber se uma barra está dentro de uma string, e o contexto de string em JSON depende de contar escapes.
Você precisa de um scanner que carregue exatamente um estado:
function stripJsonComments(text) {
let out = '';
let inString = false;
let inLine = false;
let inBlock = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
const next = text[i + 1];
if (inLine) {
if (c === '\n') { inLine = false; out += c; }
continue;
}
if (inBlock) {
// preserva as quebras de linha para os números de linha continuarem batendo
if (c === '*' && next === '/') { inBlock = false; i++; }
else if (c === '\n') { out += c; }
continue;
}
if (inString) {
out += c;
if (c === '\\') { out += next; i++; continue; } // escape, consome os dois
if (c === '"') inString = false;
continue;
}
if (c === '"') { inString = true; out += c; continue; }
if (c === '/' && next === '/') { inLine = true; i++; continue; }
if (c === '/' && next === '*') { inBlock = true; i++; continue; }
out += c;
}
return out;
}
O ramo do escape é a parte que as pessoas deixam de fora. Sem ele, a aspa escapada em "ele disse \"vá em https://example.com\" hoje" é lida como o fim da string, então o // seguinte é tomado por início de comentário e o resto da linha some.
Repare também no que isso não faz. Remover comentários deixa as vírgulas finais para trás, e elas falham por conta própria com o erro delas: o V8 relata Expected double-quoted property name in JSON at position 7 (line 1 column 8) para {"a":1,}, e o completamente diferente Unexpected token ']', "[1,2,]" is not valid JSON para [1,2,]. Uma conversão de JSONC para JSON tem que tratar os dois.
Se você preferir não carregar o scanner, cole o arquivo em Reparar JSON, que tira comentários e vírgulas finais numa passada só e te devolve JSON estrito, e depois confirme o resultado com o validador. Os dois rodam inteiramente no seu navegador, o que importa quando o arquivo que você está consertando é uma configuração de produção com credenciais dentro. As demais páginas de erros cobrem o que fazer quando a falha acaba não sendo comentário nenhum.