0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Discord の ID を JSON.parse したら末尾が 063 から 060 になった。Snowflake は BigInt で読む

0
Posted at

背景

自分のサイトに、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補助で執筆した、個人開発の紹介記事です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?