Modo Estrito e Conformidade com RFC 8259 na Validação JSON

Por Aerisium Core · ·

A gramática JSON definida na RFC 8259 é um subconjunto estrito da sintaxe de object literals do JavaScript. Ambas parecem quase idênticas, mas as diferenças causam sistematicamente falhas em produção: dados que são parseados sem erro no navegador de um desenvolvedor quebram silenciosamente consumidores downstream que executam parsers compatíveis. Entender exatamente onde as gramáticas divergem é crítico para construir pipelines de dados confiáveis.

Object Literals do JavaScript versus a Gramática JSON

A sintaxe de objetos do JavaScript evoluiu através das especificações ECMAScript, acumulando conveniências que JSON exclui deliberadamente. A RFC 8259 formaliza uma gramática restrita sem sintaxe opcional.

As incompatibilidades críticas são:

  • Trailing commas: JavaScript permite { "a": 1, "b": 2, }. A gramática JSON rejeita qualquer vírgula após o último par chave-valor. O parser espera uma vírgula apenas entre pares: após o último par, o próximo token deve ser a chave de fechamento.

  • Strings com aspas simples: JavaScript aceita 'key' e "key" indistintamente. A seção 7 da RFC 8259 determina que strings sejam delimitadas exclusivamente por aspas U+0022. Aspas simples (U+0027) não têm significado na gramática léxica do JSON.

  • Chaves sem aspas: JavaScript permite { key: "value" } onde key é tratado como um identificador. A gramática JSON exige que toda chave seja uma string entre aspas.

  • Comentários: Objetos JavaScript podem conter comentários // e /* */. A gramática JSON não possui produção de comentários. A barra (/) aparece apenas em sequências de escape.

  • Undefined e NaN: JSON.stringify do JavaScript remove silenciosamente valores undefined e converte NaN para null. Um parser que aceita esses valores como entrada produz dados que não sobrevivem a um round-trip através de JSON.parse.

Trailing Commas em Pipelines de Produção

A aceitação de trailing commas é a falha de validação JSON mais comum em sistemas reais. Considere um arquivo de configuração consumido por um processador de fluxo baseado em Java:


{
    "queue_batch_size": 1000,
    "retry_policy": "exponential_backoff",
    "dead_letter_topic": "dlq.primary",

}

A trailing comma após "dead_letter_topic" faz com que qualquer parser compatível com RFC 8259, como com.google.gson.stream.JsonReader ou com.fasterxml.jackson.core.JsonParser, lance uma exceção de parse. O processador de fluxo falha na inicialização. O deploy falha. O incidente custa horas de engenharia para diagnosticar porque o mesmo JSON é renderizado sem erro em todos os formatadores baseados em navegador que toleram trailing commas.

A causa raiz é um descompasso entre as ferramentas de desenvolvimento e o parse em produção. Desenvolvedores que trabalham no navegador nunca veem o erro porque JSON.parse() do V8 é estritamente compatível com RFC desde o ES5 e também rejeita trailing commas. A desconexão ocorre quando ferramentas intermediárias — editores, formatadores ou scripts personalizados— aceitam silenciosamente a entrada malformada antes que os dados cheguem ao parser de produção. Quando o parser estrito a rejeita, o artefato já foi implantado.

Validação em Streaming versus Construção de AST

Validar um documento JSON contra a RFC 8259 pode ser realizado em dois níveis, cada um com complexidade computacional distinta.

Um lexer em streaming executa em tempo O(n) e memória O(1). O parser classifica cada caractere sequencialmente: espaços em branco são descartados, tokens estruturais ({, }, [, ], :, ,) são validados nas posições esperadas, strings são escaneadas em busca de caracteres de controle sem escape, e números são verificados contra a gramática numérica (-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?). O lexer rejeita entrada malformada no primeiro caractere inválido e termina imediatamente.

A construção completa de AST requer espaço O(n + m), onde m é o número de nós alocados. Um documento profundamente aninhado como {"a":{"a":{"a":{"a":null}}}} aloca um novo descritor de objeto no heap do V8 para cada nível de aninhamento. Um array plano de 10 MB de primitivas produz um AST pequeno: um nó array mais N nós primitivos. Um documento de 10 MB de objetos profundamente aninhados com nomes de chave longos pode exigir de 8 a 12 vezes o tamanho bruto em bytes na memória do heap.


// Validador em streaming O(n) — sem AST, memória O(1)

function validateJson(input) {
  let i = 0;
  let depth = 0;
  let state = 'VALUE';

  while (i < input.length) {
    const ch = input[i];
    switch (state) {
      case 'VALUE':
        if (ch === '{' || ch === '[') { depth++; state = ch === '{' ? 'KEY' : 'VALUE'; i++; }
        else if (ch === '"') { state = 'STRING_END'; i++; }
        else if (ch === 't') { if (input.slice(i, i + 4) === 'true') { i += 4; state = 'SEPARATOR'; } else return false; }
        else if (ch === 'f') { if (input.slice(i, i + 5) === 'false') { i += 5; state = 'SEPARATOR'; } else return false; }
        else if (ch === 'n') { if (input.slice(i, i + 4) === 'null') { i += 4; state = 'SEPARATOR'; } else return false; }
        else if (ch === '-' || (ch >= '0' && ch <= '9')) { state = 'NUMBER'; i++; }
        else if (ch === ' ') { i++; }
        else return false;
        break;

      case 'KEY':
        if (ch === '"') { state = 'KEY_END'; i++; }
        else if (ch === '}') { depth--; state = 'SEPARATOR'; i++; }
        else if (ch === ' ') { i++; }
        else return false;
        break;

      case 'KEY_END':
        if (ch === '"') { state = 'COLON'; i++; }
        else if (ch === '\\') { i += 2; }
        else if (ch >= ' ') { i++; }
        else return false;
        break;

      case 'COLON':
        if (ch === ':') { state = 'VALUE'; i++; }
        else if (ch === ' ') { i++; }
        else return false;
        break;

      case 'STRING_END':
        if (ch === '"') { state = 'SEPARATOR'; i++; }
        else if (ch === '\\') { i += 2; }
        else if (ch >= ' ') { i++; }
        else return false;
        break;

      case 'NUMBER':
        if (ch >= '0' && ch <= '9') { i++; }
        else if (ch === '.' || ch === 'e' || ch === 'E' || ch === '-' || ch === '+') { i++; }
        else if (ch === ' ' || ch === ',' || ch === '}' || ch === ']') { state = ch === ',' ? 'VALUE' : 'SEPARATOR'; }
        else return false;
        break;

      case 'SEPARATOR':
        if (ch === ',') { state = 'VALUE'; i++; }
        else if (ch === '}' || ch === ']') { depth--; i++; }
        else if (ch === ' ') { i++; }
        else if (i >= input.length) { /* fim */ }
        else return false;
        break;
    }
  }
  return depth === 0;

}

Um validador em streaming atinge tempo O(n) e memória O(1), tornando-o adequado para validar payloads de vários gigabytes sem exaustão do heap. A contrapartida é a notificação de erros: um validador em streaming não pode informar o contexto estrutural preciso de um erro (por exemplo, “esperava-se uma vírgula na linha 42.372, coluna 15”) sem manter metadados de posição, o que empurra a complexidade para O(log n) para um rastreador de linhas.

Prevenção de Falhas Silenciosas em CI/CD

A classe mais perigosa de falha de validação JSON ocorre quando um pipeline CI/CD usa um parser permissivo durante a compilação e um parser estrito em tempo de execução.

Considere um pipeline de deploy onde um arquivo de configuração TypeScript é processado por ts-node durante a compilação, que avalia o arquivo como JavaScript, aceitando trailing commas e aspas simples. O artefato de saída é escrito como um arquivo .json. O serviço em produção lê o artefato usando encoding/json do Go, que é compatível com RFC 8259.

O artefato passa na validação de compilação porque ts-node nunca invoca JSON.parse() sobre a saída. Os valores de configuração estão corretos. A compilação é bem-sucedida. O artefato é implantado. A falha se manifesta apenas quando o serviço de produção tenta revalidar ou transformar o artefato, ou quando um sistema externo o consome. Nesse ponto o artefato já foi implantado e a reversão exige um ciclo completo de deploy.

A solução é aplicar a validação RFC 8259 no ponto mais cedo possível do pipeline: um hook de pre-commit ou uma etapa de compilação, usando um parser que corresponda ao comportamento do runtime de produção. Qualquer divergência entre o parse em tempo de compilação e em tempo de execução é dívida técnica que eventualmente se materializará como um incidente de produção. O validador confiável mais simples é o JSON.parse(): está incorporado em todos os runtimes modernos, é estritamente compatível com RFC e garante-se que corresponda a qualquer parser que acompanhe seu ambiente de produção.

JSONClear. Crafted without compromise by Aerisium.