BSON デコーダー・Extended JSON ビューア
BSON は MongoDB が保存し、やり取りするバイナリ形式です。リトルエンディアンの int32 バイト数で始まり、1 バイトの型コードと NUL 終端のフィールド名、そして型が長さを決める値が並び、最後に 0 バイトひとつで閉じます。このツールはそのバイト列をブラウザ内で直接読み、文書を三通りに同時に見せます。要素ごとの型名・型バイト・占有バイト数を並べたフィールドツリー、正準 Extended JSON、緩和 Extended JSON です。仕様が定める 21 種類の要素型すべてを扱い、古いデータでしか出会わない廃止済みの型 — undefined(0x06)、DBPointer(0x0C)、symbol(0x0E)、そしてペイロードの先頭に 2 つめの長さを抱えていて、それを忘れた読み手に無意味な 4 バイトを押し付けるバイナリのサブタイプ 0x02 — も含みます。ブラウザ上のデコーダーが最も間違えやすいのは数値です。int64 と UTC 日時、MongoDB 内部の timestamp はいずれも 64 ビットですが、JavaScript の数値は倍精度浮動小数点で整数は 9,007,199,254,740,991 までしか正確に保てません。だから 9223372036854775807 は一度通すだけで 9223372036854775808 になります。ここでは三つとも BigInt で読み、BigInt から桁をそのまま出力します。decimal128 は 128 ビットの係数とバイアス付き指数から直接、正確な十進文字列に展開し、double を経由しません。金額のために用意された型なのですから、そうでなければ意味がありません。ツリーは ObjectId を実際の 3 つの部分 — 4 バイトの生成時刻、5 バイトの乱数、3 バイトのカウンタ — に開いて見せ、UTC 日時は生の符号付きミリ秒と日付を並べて示すので、1970 年より前の日付が負になることに驚かなくて済みます。一方で推測はしません。宣言された長さがファイルと合わない、終端に届かない、仕様にない型バイトがある、あるいは間違えて渡された JSON ファイルであれば、もっともらしいツリーへ半端に解釈する代わりに、エラー名とバイト位置を示して受け付けません。
使い方
- .bson ファイルをボックスにドロップします。mongodump の出力でも、ドライバが保存した単一文書でも、MongoDB のツールが書いたものなら何でも構いません。
- ツリーを読みます。各行が 1 要素で、BSON の型名、実際に流れる型バイト、値、占有バイト数が並ぶので、文書が肥大した理由がそのまま見えます。
- 型を保ったまま持ち回るなら正準 Extended JSON を、ただ中身を読みたいだけなら緩和 Extended JSON を選びます。$numberInt と $numberLong は別物です。
- ObjectId の行の下段で生成時刻・乱数・カウンタを、UTC 日時の行の下段で生のミリ秒に対応する日付を確認します。
- 受け付けられなかったときは、どのエラーかを読みます。長さの不一致、終端の欠落、未定義の型バイトは別々の障害で、バイト位置がどこを見ればよいかを示します。
よくある質問
- 同じ配列なのに、なぜ BSON のほうが JSON よりずっと大きいのですか。
- BSON には配列専用の型がないからです。配列は、フィールド名が十進のインデックス "0"、"1"、"2" … である普通の文書として保存され、そのインデックスは実際に NUL 終端の文字列として流れます。32 ビット整数 1000 個なら値だけで 4 KB のところ、型バイト、1〜3 文字のインデックス名、終端が加わって約 8.9 KB になります。インデックスのキーが誤っていても誰も気づかない理由もここにあります。ドライバは配列を位置で読むため、キーが "0"、"2"、"x" でも要素 3 個として解けてしまい、何かが再シリアライズするまで問題が表に出ません。このツールはその状態を黙って通さず、警告として示します。
- 正準 Extended JSON と緩和 Extended JSON はどう違いますか。
- Extended JSON は BSON をテキストで書くための標準的な方法で、用途が二つあるため形式も二つあります。正準形式はすべての値を型マーカーで包みます。{"$numberInt": "1"} と {"$numberLong": "1"} と {"$numberDouble": "1.0"} は別物であり、だからこそ文書をテキストに出して同じバイト列に戻せます。緩和形式は普通の JSON で表せる値からは包みを外します。数値は数値になり、ISO 8601 の範囲に収まる日付は読める時刻になります。mongoexport の既定でずっと読みやすい一方、情報は落ちます。1 がただの 1 になってしまえば、三つある数値型のどれだったのかは分かりません。
- JavaScript で表せない BSON の値は何で、ここではどう扱っていますか。
- 三つあります。int64 と UTC 日時は符号付き 64 ビット整数、内部の timestamp は 64 ビット 1 語に符号なし 32 ビットが 2 つです。対して JavaScript の数値は倍精度で、整数を正確に保てるのは 9,007,199,254,740,991 までです。それより大きい値は取り込む時点で丸められ、多くのビューアで 9223372036854775807 が 9223372036854775808 になるのはこのためです。このデコーダーは三つとも BigInt で読み、BigInt から桁を出力するので、double を通ることがありません。decimal128 はさらに深刻で、0.1 や 2.675 を正確に保つための十進型なのに double で読んだ時点で採用した理由が消えます。ここでは 128 ビットの係数とバイアス付き指数から直接、十進文字列に展開します。
- 1970 年より前の日付が負の数で表示されるのはなぜですか。
- BSON がそう保存しているからです。UTC 日時型は Unix エポックからのミリ秒を int64 で持ち、その整数には符号があります。したがって 1970-01-01 より前の時刻はすべて負で、1953 年の生年月日はおよそ −526,608,000,000 です。正当な値でどのドライバも正しく往復させますが、このフィールドを符号なしとして扱ったり 0 で切り詰めたりするツールが意外に多く、1953 年が 1970 年になったり 2 億 9200 万年へ飛んだりします。この型が保持できる範囲は多くのソフトウェアが扱う暦よりはるかに広いため、このツールは生のミリ秒を日付の隣に並べ、暦の日付として表せない値はそうと明記します。
- バイナリのサブタイプ 0x02 とは何で、なぜ罠と呼ばれるのですか。
- 廃止された最初のバイナリ配置です。バイナリのフィールドは int32 の長さ、サブタイプ 1 バイト、続いてペイロード、という形ですが、サブタイプ 0x02 だけはペイロードの先頭にもう一つ int32 があり、その値は外側の長さよりちょうど 4 小さくなっています。他のサブタイプと同じに扱う読み手は、先頭に無意味な 4 バイトが付いた塊を返してしまい、その 4 バイトももっともらしいバイナリに見えるため誰も文句を言いません。実際にその値を使おうとしたときに初めて壊れていることが分かります。2.0 より前の古いコレクションにはまだ残っているので、BSON を読むと称するデコーダーはこの場合を特別扱いする必要があります。本ツールはそうしており、内側の長さが外側と合わない場合はそのフィールドを受け付けません。
- ObjectId の中身は何ですか。UUID ですか。
- いいえ、構造を持った 12 バイトで、たいていはその構造を知りたくて中を見ます。先頭 4 バイトはビッグエンディアンの Unix 秒タイムスタンプです。_id で並べ替えるとおおむね生成順になり、別のフィールドなしで日付で絞り込めるのはこのためです。次の 5 バイトはプロセスごとの乱数、最後の 3 バイトは任意の値から始まり文書ごとに増えるカウンタです。古いバージョンでは中央にマシン識別子とプロセス ID が入っており、サーバーの情報がわずかに漏れていましたが、3.4 で変わりました。チェックサムもバージョン欄もないので、16 進 24 文字であれば文法的にはすべて正しい ObjectId であり、妥当かどうかを示すのは埋め込まれたタイムスタンプだけです。
関連ツール
Hex Dump ビューア
テキストまたは小さなファイルをオフセット + hex バイト + 印字可能 ASCII で表示。`xxd` や `hexdump -C` 形式。
JSON to Mongoose スキーマジェネレーター
JSON オブジェクトを型マッピングされたパスを備えた Mongoose スキーマにブラウザ上で変換します。
Java .properties パーサー
java.util.Properties.load とまったく同じ手順で .properties を解析し、区切り文字・エスケープ・継続行の落とし穴を行単位で示します。
ULID デコーダー (タイムスタンプ & ランダム)
ULID を 48 ビットの生成タイムスタンプと 80 ビットのランダム値に復号 — Crockford Base32、大文字小文字を区別せず、時刻を ISO UTC・相対・エポックミリ秒で表示 — ブラウザ内で。
MessagePack デコーダ
base64 か hex の MessagePack バイト列を貼り付けると、型・入れ子のマップ・バイナリ・タイムスタンプまでブラウザ内で解読します。
ASCII コード表
256 行の ASCII / 拡張 ASCII 表 — 10進・16進・8進・2進、制御文字の名前まで。検索・コピー対応。