背景
自分のサイトに、Discord や X(旧Twitter)の ID から作成日時を取り出すツールを置いています。
作り始めたとき、テスト用の JSON を JSON.parse して ID を取り出したら、末尾の数字が変わっていました。
知識としては知っていたのですが、自分で踏むまでは他人事でした…
Snowflake は、Twitter が考えた ID の作り方だ。
64 ビットの整数の上のほうにミリ秒単位の時刻、下のほうにどのサーバーで何番目に作ったかを詰める。
Discord も同じ形で、時刻の起点(エポック)だけが違う。
結論
Snowflake ID は Number で持てません。
-
JSON.parseで数値のまま読むと、下位の桁が丸められて別の ID になる - 文字列のまま受け取り、ビット演算は
BigIntで行う - Node.js v24 なら、
JSON.parseの reviver で元の数字の文字列も取れる
JavaScript の Number は小数を含む64ビットの浮動小数点数で、整数として正確に扱えるのは Number.MAX_SAFE_INTEGER(9007199254740991、約 9×10^15)までである。
Snowflake は 18〜19 桁あるので、ここを普通に超えます。
試したこと
Discord の開発者ドキュメントに載っている ID の例 175928847299117063 を使いました。
https://docs.discord.com/developers/reference#snowflakes
API から返ってくる JSON を想定して、id を数値、id_str を文字列で持たせた文字列 raw を作り、そのまま JSON.parse しています(Node.js v24.11.1)。
const raw = '{"id":175928847299117063,"id_str":"175928847299117063"}';
console.log(JSON.parse(raw));
結果は下記でした。
{ id: 175928847299117060, id_str: '175928847299117063' }
数値で読んだ id だけ、末尾が 063 から 060 になっています。
エラーも警告も出ないので、比較して初めて気づきます。
日時だけは合ってしまう
ややこしいのは、壊れた数値から日時を取り出しても、ほぼ正しく見えることです。
Discord の Snowflake は、右に 22 ビットずらした値がエポック(2015-01-01 00:00:00 UTC、Unixミリ秒で 1420070400000)からのミリ秒だ。
下記は、壊れた Number と、文字列から作った BigInt の両方で日時を出したものです。
4194304 は 2 の 22 乗で、Number には >> で64ビットのシフトができないので割り算で代用しています。
const s = '175928847299117063'; // id_str(文字列)
const EPOCH = 1420070400000; // Discord のエポック
const viaNumber = Math.floor(Number(s) / 4194304) + EPOCH;
const id = BigInt(s);
const viaBigInt = Number(id >> 22n) + EPOCH;
console.log(new Date(viaNumber).toISOString()); // 2016-04-30T11:18:25.796Z
console.log(new Date(viaBigInt).toISOString()); // 2016-04-30T11:18:25.796Z
console.log((id >> 17n) & 0x1fn, (id >> 12n) & 0x1fn, id & 0xfffn); // 1n 0n 7n
日時は両方とも 2016-04-30 11:18:25.796 UTC で、ドキュメントの例と同じになりました。
丸められるのは下位の数ビットだけで、時刻は上位に入っているので、たまたま残るという話です。
ただ、最後の行で取り出しているワーカー番号・プロセス番号・連番(ドキュメントでは 1 / 0 / 7)は下位にあるので、Number 経由だと正しく出ません。
それに、ID 自体が別の値になっているので、データベースで探しても見つからないです。
私のツールも、入力を BigInt にしてからシフトしています。
https://hashitosystem.com/tools/snowflake/
JSON から ID を正しく取り出す
いちばん楽なのは、API が文字列でも返している項目(X の id_str など)を使うことです。
Discord の API は最初から ID を文字列で返すので、こちらは数値になる心配がありません。
自分で受け取る JSON が数値しか持っていないときは、JSON.parse の reviver(読み込み中に値を差し替える第2引数の関数)が使えます。
Node.js v24 では、reviver の第3引数 context.source に、数値に変換する前の元の文字列が入っていました。
const raw = '{"id":175928847299117063,"id_str":"175928847299117063"}';
const obj = JSON.parse(raw, (key, value, context) =>
key === 'id' && context && context.source ? BigInt(context.source) : value
);
console.log(obj); // { id: 175928847299117063n, id_str: '175928847299117063' }
key は今読んでいる項目名で、id のときだけ元の文字列から BigInt を作っています。
context が無い古い環境では value をそのまま返すので、少なくとも落ちはしません(その場合は丸められた値になります)。
この第3引数は TC39 の提案(JSON.parse source text access)で入ったもので、古いブラウザでは使えないかもしれません。
https://github.com/tc39/proposal-json-parse-with-source
BigInt はそのままだと JSON.stringify で例外になるので、保存や送信のときは String(id) で文字列に戻しておくのが無難です。
Snowflake ID は数字に見えても数値として扱わない。
受け取りは文字列、計算は BigInt、と分けておけば、桁が化ける事故は起きません。
本記事はAI補助で執筆した、個人開発の紹介記事です。