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.

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):
- Use strings para IDs em todo o pipeline:
{"id": "9007199254740993"}, o menor problema de todos; - Analise com tipos de alta precisão no servidor:
BigInteger/BigDecimaldo Java,json.Numberdo Go,decimal.Decimaldo Python; - No frontend, use o reviver de
JSON.parseou um analisador que suporte números grandes (comojson-bigint) para transformar o campo emBigInt, mas lembre queBigIntnão pode ser passado diretamente paraJSON.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
- O arquivo em si é GBK e é lido como UTF-8;
- O cabeçalho de resposta declara
charset=gbk, mas o conteúdo é na verdade UTF-8; - 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
nullversus campo omitido; - [ ] Os limites de comprimento de string dizem se contam pontos de código ou unidades UTF-16;
- [ ] Filtre
Infinity/NaN/undefinedantes da serialização, ou converta-os explicitamente emnull; - [ ] 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".