バイナリ JSON:MessagePack、CBOR、BSON の選択
要点:MessagePack、CBOR、BSON はいずれも JSON をバイト列に圧縮します。バイトコスト、int64 とバイナリの扱いを比較し、切り替える価値があるかの線引きを示します。

要点:外部との交換や、人間が検査する必要のあるデータにはテキスト JSON のままにしましょう。バイナリペイロード、2^53 を超える整数、正確な小数のせいで圧縮サイズや型の表現力が本当に問題になるときだけ、内部リンクで MessagePack か CBOR に切り替えます。BSON は MongoDB エコシステムのものです。
テキスト JSON は空白を削り、バイナリ形式は句読点と数値のテキスト表現を削ります。フィールド名は依然としてレコードごとに一度現れるので、切り替え前に実際の削減量を計算しましょう。
1. バイナリ JSON が実際に削るもの
まず期待値を合わせます。これらの形式は「別の JSON 構文」ではありません。同じデータモデルをバイト列で符号化するものです。オブジェクト、配列、文字列、数値、真偽値、null。削るものは具体的です。
- 引用符、コロン、カンマ、波括弧といった構造文字。
- 数値の十進テキスト表現。
32が 1 バイトになります。 - 文字列境界を探す再走査。固定長プレフィックスに置き換わります。
- バイナリペイロードの Base64 符号化。バイト列として直接保存できます。
削れないものも同じくらい具体的です。フィールド名は依然として各レコードに文字列として現れます。MessagePack のマップ、CBOR のマップ、BSON の文書は、いずれもエントリごとにキーを繰り返します。ですから JSON が同じ形の 1 万レコードである場合、テキストとバイナリの圧縮サイズ差は直感よりずっと小さいのです。gzip は繰り返されるフィールド名の除去が最も得意だからです。
2. 三つの形式の出自
| 形式 | 仕様と年 | 出自 | 典型的な用途 |
|---|---|---|---|
| MessagePack | msgpack.org の仕様、2008 年以降 | ミニマリズムを重視した独立設計 | キャッシュ値、IPC、ログパイプライン |
| CBOR | RFC 8949(STD 94)、2020 年 | MessagePack の影響を受け、IETF が標準化 | COSE、WebAuthn、CWT、制約のあるデバイス |
| BSON | bsonspec.org、MongoDB エコシステム | データベース文書向けに設計 | MongoDB の保存とワイヤプロトコル |
CBOR が最も標準化されています。IANA 登録の application/cbor メディアタイプ、決定論的符号化規則、記述用の CDDL があります。MessagePack は最も軽量で、仕様は一ページに収まります。BSON は最も重く、ObjectId、Decimal128、日付、バイナリサブタイプを型システムに組み込んでおり、MongoDB エコシステムの内部でしか本当に普及していない理由です。
3. 同じデータ、四つの符号化
{"name":"joe"} を例にします。バイトコストは簡単に比較できます。
| 符号化 | バイト数 | バイト列 |
|---|---|---|
| JSON テキスト | 14 | 7b 22 6e 61 6d 65 22 3a 22 6a 6f 65 22 7d |
| MessagePack | 9 | 81 a3 6e 61 6d 65 a3 6a 6f 65 |
| CBOR | 10 | a1 64 6e 61 6d 65 63 6a 6f 65 |
| BSON | 19 | 4 バイトの全長 + 型バイト + name\0 + 4 バイトの値長 + joe\0 + 終端 |
MessagePack の 9 バイトを分解します。
81:上位ニブル1000はマップ、下位ニブル0001はキーと値のペア 1 組を意味する。a3:上位 3 ビット101は長さ 3 の固定長文字列を意味する。6e 61 6d 65:nameの ASCII。a3:長さ 3 の別の文字列。6a 6f 65:joe。
CBOR の a1 はメジャータイプ 5(マップ)で長さ 1、64 はメジャータイプ 3(テキスト文字列)で長さ 4 なので、意味論は MessagePack とほぼ同型です。BSON はより大きくなります。すべての文書が 4 バイトの全長を持ち、すべてのフィールドが型バイトと名前を繰り返し、すべての文字列が長さと末尾の \0 を繰り返します。小さな文書では、BSON が JSON テキストより大きくなることも容易にあります。
これらのバイトを 16 進で表示すれば、プレフィックスを直接確認できます。
const messagePackBytes = [0x81, 0xa3, 0x6e, 0x61, 0x6d, 0x65, 0xa3, 0x6a, 0x6f, 0x65];
const hexText = messagePackBytes.map((byte) => byte.toString(16)).join(' ');
console.log(hexText); // 81 a3 6e 61 6d 65 a3 6a 6f 65
3.1 サイズとパース速度:宣伝文句は無視する
バイナリが「10 倍速い」という主張は、二つの問いに分けるべきです。
サイズ。 非圧縮なら、句読点と空白が消えるのでバイナリの方が小さいです。しかし実際のパイプラインはほぼ常に圧縮します。gzip や brotli の後では繰り返されるフィールド名が圧縮され、バイナリの優位は劇的に縮みます。JSON の圧縮と gzipの方法に従い、まず圧縮後のバイト数を測り、それから形式変更が割に合うか判断してください。
パース速度。 バイナリは字句解析(引用符、カンマ、数値文字の走査)を飛ばすので、理論上は速いはずです。しかし JavaScript では支配的なコストがオブジェクトとメモリの割り当てで、どのデコーダもそれを避けられません。データの形、フィールド数、実装の品質がすべて影響するので、信頼できる結論はベンダーの倍率を引用するのではなく、自分のペイロードでベンチマークを取ることからしか得られません。単一ペイロードが大きすぎて一度にメモリに読めないなら、符号化の利得を測る前に大きな JSON のパースとストリーミングの方法でスライスしてください。
4. int64 とバイナリフィールド:切り替える本当の理由
サイズは形式を変える理由にならないことが多いです。型の表現力が理由になります。
4.1 2^53 を超える整数
JSON の数値は JavaScript では double に落ちるので、9007199254740993 は 9007199254740992 になります。三つのバイナリ形式はこれを異なる形で扱います。
| 形式 | 64 ビット整数 | 符号なし 64 ビット | 任意精度 |
|---|---|---|---|
| MessagePack | ✓ int64(0xd3)、BigInt にデコード可能 |
✓ uint64(0xcf) |
✗(拡張型が必要) |
| CBOR | ✓ メジャータイプ 0/1 の 64 ビット形式 | ✓ 同じ符号化空間 | ✓ bignum タグ 2/3 |
| BSON | ✓ int64(0x12) |
✗ 符号付きのみ | ✗(Decimal128 は固定小数点で、任意整数ではない) |
CBOR だけが任意精度整数の標準表現を持ちます。64 ビットを超える値は、バイト文字列を囲むタグ 2(非負)またはタグ 3(負)で包まれます。MessagePack と BSON は文字列か独自の拡張規約にフォールバックします。
4.2 バイナリペイロード
JSON でバイナリを表す方法は Base64 の一つだけで、ペイロードをおよそ三分の一膨らませ、追加のデコードコピーを要します。MessagePack には bin 型(0xc4–0xc6)、CBOR にはバイト文字列(メジャータイプ 2)、BSON にはバイナリサブタイプがあります。画像、ハッシュ、ベクトル、圧縮ブロックを転送するとき、この差は句読点の数バイトを節約するよりはるかに重要です。
5. 並列比較
| 観点 | MessagePack | CBOR | BSON |
|---|---|---|---|
| 仕様 | コミュニティ仕様 | IETF RFC 8949 | MongoDB エコシステム |
| メディアタイプ | application/msgpack(IANA 標準ではない) |
application/cbor(IANA 登録) |
汎用メディアタイプなし |
| 型システム | JSON の 6 型に bin と ext を加えたもの | JSON の 6 型に bin、タグ、単純値を加えたもの | より広い:ObjectId、Decimal128、日付、正規表現、タイムスタンプ |
| 拡張機構 | 拡張型(例:timestamp -1) | タグ(日付、bignum、埋め込み CBOR) | 固定の型コード、エコシステム規約による拡張 |
| 決定論的符号化 | 標準的な要求なし | 明示的な規則、署名に使える | なし |
| 自己記述 | あり。ただしフィールド単位のスキーマはない | あり。CDDL で記述可能 | あり。フィールド名を文書ごとに繰り返す |
| 小文書のオーバーヘッド | 低い | 低い | 高い(長さプレフィックス、フィールド名、終端) |
| エコシステム | キャッシュ、ログ、一部の RPC | COSE、WebAuthn、IoT | MongoDB のみ |
6. ブラウザと Node の対応
Node 24 はこれらのコーデックを同梱していないので、ライブラリを入れます。よくある選択肢:
| ランタイム | MessagePack | CBOR | BSON |
|---|---|---|---|
| Node.js | @msgpack/msgpack、msgpackr |
cbor-x、borc |
bson |
| ブラウザ | @msgpack/msgpack |
cbor-x のブラウザビルド、cbor-web |
bson |
どちらの系統も Uint8Array で動作し、Buffer を必要としません。バージョン差に注意してください。
import { encode, decode } from '@msgpack/msgpack';
/**
* オブジェクトを MessagePack バイト列に符号化し、検証のためにすぐデコードする。
* @param payload 符号化する素のオブジェクト
* @returns デコードされた値と符号化後のバイト長
*/
function roundTrip(payload: Record<string, unknown>): { decoded: unknown; byteLength: number } {
const bytes = encode(payload);
return { decoded: decode(bytes), byteLength: bytes.byteLength };
}
ほとんどのライブラリは既定で大きな整数を number にデコードし、安全な範囲を超えると精度を失います。正確な値が重要なら BigInt オプションを明示的に有効化してください(@msgpack/msgpack の useBigInt64、cbor-x の同等のスイッチ)。さもないと JSON の古い問題を新しい形式に移しただけです。
7. デバッグ性とプロトコル互換性
7.1 デバッグ性:最大の隠れたコスト
テキスト JSON の最大の利点はサイズではなく、いつでも開けることです。バイナリに切り替えると:
grep、awk、jqがすべて動かなくなり、ログパイプラインに追加のデコード手順が必要になる。- 本番のデバッグには正しいバージョンのデコーダが必要で、さもないと見えるフィールド名が間違う。
- キャプチャした通信はブラウザの開発ツールで読めないバイトの塊になる。
- コードレビューの差分は 16 進の羅列になる。
CBOR には診断記法があり、バイトを JSON に近い読みやすいテキストに戻せます(マップなら {1: 2} など)。MessagePack にもいくつかのコマンドラインデコーダがあります。しかしどれも追加インストールが必要で、標準の操作経路の外にあります。
結論:人間が頻繁にデータを検査する必要があるなら、バイナリ形式の隠れたコストは節約する帯域を上回ります。
7.2 プロトコル互換性と拡張
- CBOR は最も完全なタグ機構を持ちます。タグ 0 は日時文字列、タグ 1 は Unix タイムスタンプ、タグ 2 と 3 は bignum、タグ 24 は埋め込み CBOR。決定論的符号化と組み合わせ、署名や実装間の一貫性を要する COSE、WebAuthn、CWT のようなプロトコルで使われます。
- MessagePack は拡張型を実装者に委ねます。実際に一般的なのは timestamp(拡張型 -1)だけです。チームをまたぐと、すべての拡張型をプロトコル文書に書かなければ相手がデコードできません。
- BSON は固定の型コードを持ち、
ObjectId、Decimal128、MinKey、MaxKeyは MongoDB 固有の意味論です。BSON を汎用交換形式として扱うと、使わない型と使わない長さプレフィックスを大量に持ち込むことになります。
プロトコルが「同じデータは同じバイト列を生まなければならない」と要求するなら、標準の答えを提供するのは CBOR だけです。MessagePack と BSON はフィールド順と符号化の詳細について追加の合意が必要です。
8. 切り替える価値があるとき
切り替えを正当化するシグナル:
- ペイロードが大きく、両端を変更でき、圧縮後のサイズがなお意味のある差を持つ。
- バイナリ内容を転送し、Base64 の膨張とデコードコストが測定可能。
- JSON の double では表せない 64 ビット整数や正確な小数が必要。
- リンクがサーバー間で、診断ツールを標準化できる。
反対するシグナル:
- gzip や brotli がすでに導入済みで、利得が一桁台のパーセント。
- API が公開されていて、第三者は汎用 HTTP クライアントしか持たない。
- データが人間の手で読むログや設定。
- チームにデコーダとバージョン互換性を保守する余裕がない。
中間の道も有効です。外部には JSON を保ち、内部キャッシュに MessagePack を使います。キャッシュ値は自分のサービスだけが読むので、サイズとパースコストの両方が改善し、問題が起きてもキャッシュを消せば契約に触れずに解決します。
9. チェックリスト
- [ ] 実データの圧縮後バイト数で比較し、非圧縮の数値で比較しない。
- [ ] 大きな整数の方針を決める:BigInt オプション、文字列転送、CBOR の bignum。
- [ ] バイナリフィールドは Base64 で包まず、bin やバイト文字列として送る。
- [ ] 拡張型とタグをプロトコル文書に書き、デコーダのバージョンを固定する。
- [ ] 本番障害時に有効化できるテキスト JSON のデバッグ経路を保つ。
- [ ] 口頭の主張ではなく、ベンチマークを実行して再現可能な数値を記録する。
MessagePack と CBOR の違いは何ですか?
符号化は非常によく似ています。マップ、配列、文字列はいずれもコンパクトな型+長さのプレフィックスを使います。主な違いは標準化です。CBOR は IETF RFC で、IANA メディアタイプ、タグ機構、決定論的符号化規則を持ちます。MessagePack はコミュニティ仕様で、拡張型は私的な合意に依存します。署名、実装間の一貫性、制約デバイスのプロトコルが必要なら CBOR を選んでください。
BSON は汎用形式として JSON を置き換えられますか?
おすすめしません。BSON はデータベース文書向けに設計され、すべての文書が 4 バイトの全長を持ち、すべてのフィールドが型と名前を繰り返すため、小さな文書は JSON テキストより大きくなります。さらに ObjectId や MinKey のような MongoDB 固有の型を含みます。MongoDB の外では型システムの大半が使われないまま、サイズのペナルティだけが残ります。
バイナリ形式にするとペイロードはどれくらい小さくなりますか?
データの形によります。フィールド名が激しく繰り返されるなら、圧縮後のテキスト JSON とバイナリ形式は近い値になります。名前が短く、データが構造文字と数値で満ちているなら、バイナリの優位はより明確です。汎用の倍率で見積もるのではなく、実際のサンプルを非圧縮と gzip 後の両方で測って判断してください。
バイナリデータを jq でデバッグできますか?
いいえ。jq はテキスト JSON しか処理せず、MessagePack や CBOR のバイト列では即座に失敗します。対応するライブラリで JSON にデコードしてから jq に渡す必要があります。CBOR は診断記法ツールを通して読めるテキストにもできます。テキストのデバッグ経路を保つ方が、必要になるたびにツールを入れるより安上がりです。