Por que os números mudam ao passar por um programa
Números parecem a coisa mais segura de transportar entre sistemas, e são o que mais frequentemente chega sutilmente errado. O motivo é que o tipo numérico usual é um double — uma fração binária com 53 bits de precisão — enquanto os valores que interessam às pessoas são frações decimais e identificadores longos, e esse formato não guarda nenhum dos dois com exatidão. As falhas são silenciosas porque a exibição arredonda o estrago até ele sumir.
Frações decimais não existem em binário
Um terço não tem forma decimal exata: você escreve 0,3333 e para em algum ponto. Em binário acontece o mesmo com um décimo. O valor armazenado mais próximo de 0.1 é 0.1000000000000000055511151231257827021181583404541015625, e o mais próximo de 0.2 também está levemente deslocado, então somá-los cai num valor que é impresso como 0.30000000000000004.
Isso não é um defeito de uma linguagem específica. Todo ambiente que use o mesmo formato de 64 bits dá a mesma resposta, porque a resposta é uma propriedade do formato. O que é exato em binário são os inteiros e as frações cujo denominador é uma potência de dois, então metade e um quarto se comportam perfeitamente, e um décimo nunca vai se comportar.
Acima de nove quatrilhões os inteiros deixam de ser consecutivos
Um double guarda inteiros exatos até 2 elevado a 53, que é 9007199254740992. O seguinte não pode ser representado: peça 9007199254740993 e você recebe 9007199254740992, em silêncio. Daí em diante os inteiros representáveis ficam espaçados de dois em dois, depois de quatro em quatro, e assim por diante.
Esse limiar cai bem no meio de um tipo de dado comum. Identificadores de sessenta e quatro bits — chaves de banco de dados, ids de mensagens e publicações das grandes plataformas, ids no estilo snowflake — passam dele rotineiramente, então um id que atravessa um double sai como um número vizinho: ainda com cara de id, mas apontando para nada.
JSON é onde isso costuma acontecer
O JSON em si não limita o tamanho de um número; o formato é texto e os dígitos estão todos lá. Quem os perde é o analisador, porque a maioria transforma cada número em um double à medida que lê. A corrupção acontece, portanto, do lado de quem recebe, e os registros de quem enviou parecem impecáveis.
A regra prática é levar identificadores grandes como strings. Um id não é uma quantidade — você nunca soma dois — então nada se perde ao colocá-lo entre aspas, e as aspas são a única coisa que sobrevive a uma ida e volta por analisadores arbitrários. Se não der para mudar a mensagem, ao menos compare o id recebido com o texto bruto, e não com o número já analisado.
Dinheiro é o outro lugar onde isso morde
Preços são frações decimais, então herdam todos os problemas acima: um décimo não é exato, três vezes um décimo não é três décimos, e uma soma longa vai derivando por frações de centavo que acabam arredondando para uma diferença visível. Guarde dinheiro como um número inteiro da menor unidade, ou em um tipo decimal feito para isso.
O arredondamento é uma decisão à parte que as pessoas supõem universal, e não é. Arredondar a metade para cima e arredondar a metade para o par mais próximo têm ambos longa história na contabilidade, e linguagens e planilhas diferentes escolhem padrões diferentes — é assim que dois sistemas que concordam em todas as entradas discordam num total.
A exibição esconde o estrago
Ao imprimir um double, normalmente se mostra o texto mais curto que é lido de volta como o mesmo valor, então um número levemente errado costuma sair como o valor limpo que você esperava. Dois números podem aparecer idênticos e ainda assim falhar num teste de igualdade — exatamente a situação que faz essa classe de bug parecer sobrenatural.
Então compare no mesmo terreno. Para ver o valor realmente armazenado, imprima com dezessete algarismos significativos ou converta para decimal; e para comparar dois valores calculados, admita uma tolerância proporcional à magnitude em vez de exigir igualdade exata.
Idas e voltas perdem dígitos se você não escrever o bastante
Converter um double em texto e de volta só é exato se o texto trouxer dezessete algarismos significativos; para um float de 32 bits o número é nove. Qualquer coisa que formate com seis casas decimais por legibilidade — uma linha de log, uma exportação em CSV, um arquivo de configuração — jogou fora parte do valor, e lê-lo de volta dá um número diferente do original.
O mesmo vale para reduzir a precisão de propósito. Um float de 32 bits guarda cerca de sete algarismos significativos, então empurrar um double por ele e voltar deixa 0.1 como 0.100000001490116119384765625. Isso serve para uma coordenada num jogo e não serve para uma medida que você vai somar um milhão de vezes.
O que checar quando um número parece errado
Teste os limites de propósito em vez de esperar pela surpresa. Mande 0.1 mais 0.2 pelo caminho e veja se os dígitos extras aparecem; mande 9007199254740993 e veja se ele volta igual; mande um valor com mais de dezessete algarismos significativos e veja o que sobrevive.
Quando algo estiver fora, descubra qual etapa perdeu a informação: a exibição, o analisador, uma conversão para float ou a própria aritmética. Cada uma tem uma correção diferente — imprimir mais dígitos, colocar o id entre aspas, usar um tipo mais largo ou migrar para inteiros — e aplicar a errada deixa o problema no lugar e ainda mais difícil de enxergar.