~/guides/json-big-numbers-precision

JSON

Почему JSON.parse портит большие числа и что с этим делать

Разбор потери точности int64 идентификаторов при парсинге JSON в JavaScript и три рабочих способа обойти проблему.

Однажды в продакшне обнаруживается, что заказ с id 9007199254740993 в базе данных существует, а по этому же id в JavaScript приложении найти его не получается. Не баг базы, не баг API. Баг в самой природе чисел JavaScript, и JSON тут просто оказался посыльным, который принес плохую новость.

Откуда растут ноги проблемы

JavaScript хранит все числа как 64-битные числа с плавающей точкой формата IEEE 754, независимо от того, целое это число или дробное. У такого представления есть жесткий предел точности для целых чисел, Number.MAX_SAFE_INTEGER, что равно 9007199254740991. Выше этого значения соседние целые числа физически не могут быть представлены по отдельности, они начинают округляться до ближайшего представимого значения.

console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(9007199254740993 === 9007199254740992); // true, это не опечатка

Базы данных вроде PostgreSQL с типом bigint или Twitter с их знаменитыми snowflake id спокойно оперируют числами куда больше этого предела. Как только такое число проходит через JSON.parse, оно тихо теряет точность, без единого предупреждения или ошибки.

Демонстрация проблемы

const raw = '{"orderId": 9007199254740993}';
const parsed = JSON.parse(raw);
console.log(parsed.orderId); // 9007199254740992, число изменилось

Обратите внимание, ошибки не было. JSON.parse не считает это проблемой, потому что синтаксически число валидно, просто JavaScript физически не может сохранить именно это значение с точностью до единицы.

Способ первый: строка вместо числа в самом API

Самое надежное и самое часто рекомендуемое решение, отдавать большие идентификаторы строками с самого начала, а не числами.

{ "orderId": "9007199254740993" }
Подход Плюс Минус
Число в JSON Компактнее, привычнее Теряет точность выше MAX_SAFE_INTEGER
Строка в JSON Полностью безопасно Нужно явно приводить к числу при математике
BigInt через ревайвер Точность сохраняется Требует поддержки BigInt всем кодом дальше по цепочке

Строка это по сути konsensus индустрии для действительно больших идентификаторов, GitHub API, Twitter API и большинство других сервисов с snowflake-подобными id отдают их именно строками.

Способ второй: reviver функция с BigInt

Если менять формат API невозможно, а точность критична, можно перехватить парсинг через второй аргумент JSON.parse.

const raw = '{"orderId": 9007199254740993}';
const parsed = JSON.parse(raw, (key, value, context) => {
  if (typeof value === "number" && !Number.isSafeInteger(value)) {
    return BigInt(context.source);
  }
  return value;
});

Стоит учесть: context.source в reviver доступен только в достаточно новых версиях движка JavaScript, и придется дальше по всему коду аккуратно работать с типом BigInt, который не сериализуется обратно через обычный JSON.stringify без отдельной обработки.

Способ третий: специализированный парсер

Библиотеки вроде json-bigint парсят JSON вручную, а не через встроенный движок, и позволяют явно указать, что большие числа нужно представлять либо строкой, либо BigInt, без риска молчаливой потери точности где-то на полпути.

import JSONbig from "json-bigint";
const parsed = JSONbig.parse(raw);
console.log(parsed.orderId.toString()); // 9007199254740993, без искажения

Как быстро проверить, есть ли у вас эта проблема

Возьмите ваш реальный самый большой идентификатор из базы, оберните в JSON, и вставьте на страницу JSON Formatter, провалидируйте, а после откройте devtools console и сравните строковое представление до и после. Если числа совпадают, вам повезло, диапазон id пока в безопасной зоне. Если нет, самое время выбрать один из трех способов выше, пока это не всплыло в проде в пятницу вечером.

Итоговый чеклист

  • Если контролируете формат API, отдавайте id больше 2^53 строками сразу, не оставляйте это на волю парсера
  • Если формат API чужой, добавьте reviver или специализированную библиотеку на своей стороне
  • Проверяйте реальные максимальные значения id в вашей базе данных, а не полагайтесь на то, что “у нас пока маленькие числа”
  • Помните, что проблема касается не только id, но и любых больших чисел, включая финансовые суммы в минимальных единицах вроде копеек или центов при достаточно больших оборотах