JSON Toolkit

Seis tipos, precisão numérica e armadilhas de codificação

Resumo: O JSON tem apenas seis tipos, mas cada um deles pode remodelar seus dados silenciosamente. Este artigo cobre as armadilhas de números, strings e null de uma vez.

Iniciante~12 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Capa de Seis tipos, precisão numérica e armadilhas de codificação

Resumo da ópera: verifique primeiro se campos de ID ou de dinheiro ultrapassam 2^53 - 1 ou exigem muitas casas decimais, depois se os limites de string contam pontos de código ou unidades UTF-16; transporte qualquer valor fora da faixa de inteiro seguro como string em vez de confiar que o analisador preserve a precisão.

O JSON tem menos tipos do que a maioria pensa, e mais armadilhas: números mudam, strings quebram e null é algo diferente de uma chave ausente.

1. O panorama dos tipos

Tipo Exemplo de literal Restrição no JSON Mapeamento comum
Objeto {"a": 1} Chaves precisam ser strings, valores podem ser qualquer coisa Object / Map / struct
Array [1, "a", null] Elementos podem ser heterogêneos Array / List / slice
String "text" UTF-8, aspas duplas String
Número 1, -1.5, 1e9 Sem zeros à esquerda, sem +, sem NaN double / Decimal
Booleano true / false Minúsculo bool
null null O único valor nulo null / Option::None

Repare nas duas coisas que não existem: não há tipo de data e não há tipo inteiro. Todas as datas são strings, e todos os inteiros e valores de ponto flutuante compartilham o único tipo "número".

2. Números: precisão dupla é a resposta padrão

2.1 A fronteira do inteiro seguro

O maior inteiro que a precisão dupla IEEE 754 consegue representar exatamente é 2^53 - 1 = 9007199254740991. Além disso, aparecem lacunas entre inteiros adjacentes:

JSON.parse('{"id": 9007199254740993}').id
// 9007199254740992

JSON.stringify({ id: 9007199254740993 })
// '{"id":9007199254740992}'

Isso não é um problema do JSON; é um problema da etapa "texto → precisão dupla". Qualquer pipeline com JavaScript no meio (gateway, BFF, frontend, serviço Node) vai esbarrar nisso.

Remédios (ordenados pelo quanto eu recomendo):

  1. Use strings para IDs em todo o pipeline: {"id": "9007199254740993"}, o menor problema de todos;
  2. Analise com tipos de alta precisão no servidor: BigInteger / BigDecimal do Java, json.Number do Go, decimal.Decimal do Python;
  3. No frontend, use o reviver de JSON.parse ou um analisador que suporte números grandes (como json-bigint) para transformar o campo em BigInt, mas lembre que BigInt não pode ser passado diretamente para JSON.stringify.

2.2 Decimais não são valores exatos

0.1 + 0.2            // 0.30000000000000004
JSON.stringify(0.1 + 0.2)
// "0.30000000000000004"

Sempre represente valores monetários como inteiros na menor unidade da moeda (centavos) ou como strings; nunca os acumule com ponto flutuante.

2.3 Armadilhas de números na serialização

JSON.stringify({ a: 1e21 })        // {"a":1e+21}
JSON.stringify({ a: -0 })          // {"a":0}
JSON.stringify({ a: Infinity })    // {"a":null}
JSON.stringify({ a: NaN })         // {"a":null}
JSON.stringify({ a: 10n })         // TypeError: Do not know how to serialize a BigInt

Infinity e NaN são transformados silenciosamente em null, a classe mais difícil de "os dados simplesmente desapareceram" de se rastrear.

2.4 Comportamento padrão por linguagem

Linguagem Tipo numérico padrão Casos que exigem tratamento explícito
JavaScript double Inteiros grandes, serialização de BigInt
Go float64 (encoding/json) Use json.Number ou Decoder.UseNumber() para preservar a precisão
Java (Jackson) int/long/double/BigDecimal inferido do literal Force DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
Python int (precisão arbitrária) / float Use parse_float=Decimal para preservar a precisão decimal
Rust (serde_json) i64/u64/f64 Precisão arbitrária exige habilitar arbitrary_precision

3. Strings: um fluxo de bytes UTF-8 visto através de UTF-16

3.1 Comprimento não é o mesmo que número de caracteres

Em JavaScript, "🎉".length é 2, porque emojis vivem nos planos suplementares e são representados por um par substituto. Se o servidor limita o comprimento por "número de caracteres" enquanto o frontend valida por length, os dois lados vão discordar com certeza.

'🎉'.length                 // 2
Array.from('🎉').length     // 1
[...'👨‍👩‍👧'].length           // 5 (incluindo joiners de largura zero)

Quando precisar de contagem rigorosa, use Intl.Segmenter (por cluster de grafemas) ou conte pontos de código Unicode no backend, e deixe a definição clara na documentação da API.

3.2 Substitutos isolados

Um substituto alto isolado como "\ud800" é gramaticalmente JSON válido (\uXXXX aceita quaisquer quatro dígitos hexadecimais), mas não é texto Unicode válido. Desde o ES2019, JSON.stringify escapa substitutos isolados como \ud800, garantindo que a saída seja "bem formada". Ao transferir entre linguagens, valores assim serão rejeitados se o outro lado validar Unicode rigorosamente.

3.3 \u2028 e \u2029

Separadores de linha e de parágrafo são válidos em JSON, mas inseri-los diretamente no código-fonte JavaScript ou em uma tag <script> causa erro de sintaxe. Ao embutir JSON em HTML, sempre escape-os ou passe a analisar a partir de uma string com JSON.parse.

3.4 Três origens de texto chinês embaralhado

  1. O arquivo em si é GBK e é lido como UTF-8;
  2. O cabeçalho de resposta declara charset=gbk, mas o conteúdo é na verdade UTF-8;
  3. Passa por um middleware que "decodifica para string e depois codifica de novo", então o texto em chinês é transcodificado uma segunda vez.

Ordem de investigação: olhe primeiro os bytes (xxd), depois o cabeçalho de resposta e, por fim, o middleware.

4. null versus "esta chave não existe"

Em JSON esses dois são diferentes:

{"nickname": null}
{}
  • O primeiro significa "o campo existe, com um valor vazio";
  • O segundo significa "o campo não existe".

Em TypeScript isso se torna a diferença entre string | null e nickname?: string; em Java se torna a diferença entre "null explícito" e "valor padrão". Sem uma convenção acordada, surgem estas divergências:

// Abordagem do frontend A
if (data.nickname === undefined) { /* trata como sem apelido */ }
// Abordagem do frontend B
if (!data.nickname) { /* trata null e string vazia juntos */ }

Recomendação: a documentação da API deve declarar "use null de forma uniforme quando um valor estiver ausente, nunca omita a chave", ou o contrário — mas escolha apenas uma opção; ao atualizar uma API, use a semântica de PATCH para evitar a ambiguidade de "não foi enviado ou foi enviado como null".

5. Arrays e objetos

5.1 Arrays não são "homogêneos"

[1, "two", {"three": 3}, null]

Gramaticalmente válido, mas a definição de tipo fica muito difícil de escrever. Em APIs, procure manter os elementos do array homogêneos; para casos heterogêneos, mude para um array de objetos com um campo discriminante type.

5.2 Chaves de objeto são não ordenadas, mas muita gente depende da ordem

A especificação diz que um objeto é "uma coleção não ordenada de pares nome/valor". Na prática, o V8 preserva a ordem de inserção (exceto chaves inteiras, que são ordenadas). Construir lógica sobre a ordem das chaves (pegar "a primeira chave", por exemplo) significa depender de um detalhe de implementação.

5.3 Arrays esparsos e lacunas não existem

O [1, , 3] do JavaScript é serializado como [1,null,3] — a lacuna se torna null. O JSON não tem conceito de "slot".

6. Uma lista de verificação acionável

  • [ ] Todos os IDs, valores monetários e inteiros grandes são strings explícitas no contrato da API;
  • [ ] A documentação da API declara a semântica de null versus campo omitido;
  • [ ] Os limites de comprimento de string dizem se contam pontos de código ou unidades UTF-16;
  • [ ] Filtre Infinity / NaN / undefined antes da serialização, ou converta-os explicitamente em null;
  • [ ] O JSON embutido em HTML é escapado;
  • [ ] Frontend e backend têm, cada um, um caso de teste de contrato cobrindo "inteiro grande + emoji + null".

Perguntas frequentes

Como lidar com inteiros acima de 2^53 - 1?

A abordagem de menor problema é enviar campos como IDs como strings em todo o pipeline, por exemplo {"id": "9007199254740993"}. No servidor você pode preservar a precisão com BigInteger / BigDecimal do Java, json.Number do Go ou decimal.Decimal do Python. No frontend, um analisador como json-bigint pode transformar o campo em BigInt, mas lembre que BigInt não pode ser passado diretamente para JSON.stringify.

Por que Infinity e NaN viram null depois da serialização?

O tipo numérico JSON não aceita literais não padrão como NaN e Infinity, então JSON.stringify os converte silenciosamente em null, sem nenhum erro. Esta é a classe mais difícil de "os dados simplesmente desapareceram" de se rastrear. Filtre esses valores explicitamente antes da serialização, ou converta-os em null conforme a convenção do seu negócio e registre isso no log.

Devo enviar null ou omitir o campo completamente?

Qualquer um funciona, mas a documentação da API precisa escolher uma opção e declarar isso claramente, caso contrário cada frontend vai presumir algo diferente. Nos exemplos, {"nickname": null} e {} mapeiam para string | null e nickname?: string em TypeScript. Use a semântica de PATCH ao atualizar uma API para evitar a ambiguidade de "não foi enviado ou foi enviado como null".