Почему большие целые теряют точность
Главное: Целые выше 2^53-1 теряют точность в JSON.parse. Snowflake ID, номера заказов и bigint из базы данных все с этим сталкиваются, поэтому храните их как строки от начала до конца.

Главное: относитесь к snowflake ID, номерам заказов и bigint-первичным ключам как к строкам на всём пути сериализации, передачи и парсинга; сохраняйте числовой тип только когда поле доказанно остаётся в пределах 2^53-1, и тогда закрепите это ограничением maximum в Schema.
Потому что повреждение происходит во время парсинга, молча.
JSON.parse('{"id": 9007199254740993}');
// { id: 9007199254740992 }
Пропавшая цифра исчезла; восстанавливать нечего.
Откуда берётся 2^53
В JavaScript один числовой тип: двойная точность IEEE 754, с 53 битами мантиссы (один неявный, 52 явных). Поэтому точные целые заканчиваются на ±(2^53-1):
-9007199254740991 .. 9007199254740991
Выше этой границы соседние представимые значения отличаются на 2, 4, 8. 9007199254740993 нечётно, а на такой величине каждое представимое значение чётно, поэтому оно округляется до 9007199254740992.
Какие поля страдают
| Поле | Цифры | Риск |
|---|---|---|
| Snowflake ID | 18–19 | Высокий, последние цифры меняются |
| Номера заказов / транзакций | 16–20 | Высокий |
| Bigint-первичные ключи БД | 19 | Высокий |
| Деньги в минорных единицах | — | Низкий |
| Метки времени в миллисекундах | 13 | Безопасно |
Общая нить: это идентификаторы, а не количества. Идентификатор передаётся, но никогда не складывается и не умножается, поэтому строка — правильный тип.
Четыре исправления
Сериализуйте как строку. Бэкенд конвертирует bigint-поля перед записью ответа. Просто и надёжно; документируйте, что эти поля — строки.
Позвольте сериализатору сделать это. ToStringSerializer для Long в Jackson, тег json:",string" в Go. Та же идея: держите значение вне числового пути.
Парсите с поддержкой BigInt. json-bigint и подобные библиотеки. Учтите, что BigInt нельзя смешивать с арифметикой Number, а JSON.stringify его отвергает.
Ограничьте диапазон. Если поле действительно не может превысить 2^53, добавьте maximum в Schema, чтобы данные вне диапазона не проходили валидацию, а не падали молча в браузере.
Как понять, есть ли у вас этот баг
Возьмите известный ID и прогоните его туда-обратно: сериализуйте, передайте, разберите, сравните символ за символом. Или просмотрите сырое тело ответа — некавыченное 19-значное число, приходящее с неверными последними цифрами, почти наверняка потеряно при парсинге.
Вопросы и ответы
Почему JSON.parse меняет последние цифры без ошибки?
В JavaScript один числовой тип — двойная точность IEEE 754, поэтому целые выше 2^53-1 могут попадать только на ближайшее представимое значение. JSON.parse трактует это как обычное числовое преобразование: он ни выбрасывает, ни предупреждает.
Как фронтенд может сохранить большие целые нетронутыми?
Используйте библиотеку вроде json-bigint, потому что JSON.parse этого не может. Цена — BigInt нельзя смешивать с арифметикой Number, а JSON.stringify его отвергает, поэтому окружающий код тоже приходится менять.
Должна ли документация API указывать, что эти поля — строки?
Да. Сериализация в строки — самое простое и надёжное исправление, но оно работает только если потребители трактуют значение как текст, поэтому тип поля нужно записать в контракт API.