JSON Toolkit

構文と形式 FAQ:コメント、末尾カンマ、精度

要点:コメントは許されるか、末尾カンマがなぜエラーか、キーに引用符は必要か、9007199254740993 が変わる理由。十二の結論を RFC 8259 に照らして確認します。

初級約 12 分で読めます2026-10-30 公開2026-10-30 更新
構文と形式 FAQ のカバー画像

要点:プロセス境界を越えるものには、素の RFC 8259 JSON を書く — 二重引用符、コメントなし、末尾カンマなし、BOM なしの UTF-8。コメントが必要な設定には JSONC か JSON5 を使い、実行時に標準 JSON へ戻します。2^53-1 を超える整数はサーバーで文字列としてシリアライズします。

これらの質問に共通するのは、「動くはず」に見えるのに、仕様は実は非常に厳格だということです。

1. 何が正しい JSON か

JSON、JSON5、JSONC の違いは?

標準 JSON は二重引用符の文字列だけを認識し、コメントも末尾カンマも許しません。JSON5 と JSONC はその上に機能を足します。

機能 JSON(RFC 8259) JSONC JSON5
二重引用符の文字列 対応 対応 対応
単一引用符の文字列 非対応 非対応 対応
コメント 非対応 対応 対応
末尾カンマ 非対応 非対応 対応
引用符なしキー 非対応 非対応 対応
16 進数 非対応 非対応 対応
NaN / Infinity 非対応 非対応 対応
拡張子 .json .jsonc .json5

選択の原則は単純です。プロセス境界を越えるときは常に標準 JSON を使い、設定ファイルはエディタの対応に応じて JSONC か JSON5 を使ってかまいません。 tsconfig.json と .vscode/settings.json がコメントを含めるのは、エディタが JSONC としてパースするからで、厳格な JSON.parse に渡すと依然エラーになります。

JSON にコメントは書けるか

書けません。RFC 8259 の文法にコメント生成規則がないので、// や /* */ はパースを失敗させます。

設定に説明を残したいときは三つの代替があります。

{
  "_comment": "以下のタイムアウトの単位はミリ秒",
  "timeout": 3000,
  "retry": {
    "$doc": "リトライ回数。0 はリトライしない",
    "count": 3
  }
}
  • _comment のような、読み手が単に無視する慣習フィールドを使う。
  • $ 接頭辞のフィールドを使い、業務フィールドと衝突する可能性を減らす。
  • 設定は素直に JSONC か YAML に切り替え、実行時に標準 JSON へ変換する。

末尾カンマがエラーになるのはなぜか

文法で配列もオブジェクトも「要素+区切り」の構造であり、最後の要素の後にカンマが許されないからです。

{
  "a": 1,
  "b": 2,
}

上の断片は } の前に余分なカンマがあり、エラーはパーサーの実装に応じてカンマか閉じ括弧で報告されます。 これは JavaScript から移行するときの最も簡単な罠でもあります。JS のオブジェクトリテラルは末尾カンマを許し、JSON は許しません。

単一引用符は許されるか

いいえ。文字列は二重引用符を使わなければならず、'text' は構文エラーです。

データが Python の repr()、シェルスクリプト、手書き設定からコピーされたなら、パーサーが寛容だと期待するのではなく、 まずツールで変換してください。

キーを引用符なしにできるか

いいえ。オブジェクトのメンバー名は二重引用符で囲んだ文字列でなければなりません。

{ name: "json" }

これは JavaScript のオブジェクトリテラルで、JSON ではありません。引用符を足せば正しくなります。

{ "name": "json" }

トップレベルは配列や文字列でよいか

はい。RFC 8259 はトップレベルに任意の JSON 値を許します。オブジェクト、配列、文字列、数値、true、false、null はすべて正しいです。

ただし実務では、トップレベルにオブジェクトか配列だけを使うことを推奨します。理由は二つ。

  1. 互換性:古いパーサー(RFC 4627 時代)はトップレベルがオブジェクトか配列であることを要求します。
  2. セキュリティ:トップレベル配列は歴史的に JSON ハイジャックのリスクを運びました。現代のブラウザは修正済みですが、古いシステムはなおその慣習を仮定します。

ですから "hello" も正しい JSON で ["a", "b"] もそうですが、{"data": [...]} を返す API の方が安全です。

2. エンコーディングと文字列

ファイル先頭の BOM はパースを壊すか

壊します。RFC 8259 は JSON テキストを UTF-8 で送信することを明示的に要求し、バイトオーダーマーク(BOM)を付けてはならないとしています。

BOM はファイルのごく先頭の 3 バイト EF BB BF です。完全に見えませんが、文字列の最初の位置を占めるので、 パーサーは「最初の文字が { でない」と結論してエラーを出します。報告される行と列は通常 1 行 1 列目で、不可解に見えます。

Windows のメモ帳の「UTF-8 で保存」は既定で BOM を含みます。確認と除去:

# 確認:efbbbf で始まる出力は BOM ありを意味する
head -c 3 data.json | xxd

# 除去:tail で最初の 3 バイトを飛ばす
tail -c +4 data.json > clean.json

現代の JavaScript の JSON.parse は BOM を許容します(V8 が内部で飛ばします)が、Java、Go、Python の厳格な実装は許容しません。 「同じファイルが言語によって異なる結果になる」よくある理由です。

日本語を \uXXXX に変換する必要があるか

ありません。RFC 8259 は JSON テキストを UTF-8 で要求するので、日本語は直接現れてよく、{"name": "张三"} は完全に正しいです。

\uXXXX は同等の代替表記にすぎず、文字を直接表せないチャネルに使います。経験則:

  • HTTP リクエストボディ、ファイル、データベース:日本語を直接書き、1 文字 3 バイトを節約する。
  • HTTP ヘッダー、URL パラメータ、別の文字列への埋め込み:エンコーディング問題を避けるためエスケープを使う。

必ずエスケープしなければならないのは制御文字(U+0000 から U+001F)だけです。たとえば改行は \n と書かねばならず、 文字列内に生の改行を置けません。

絵文字は直接文字列に入れられるか

入れられますが、UTF-16 コードユニット二つを占めることを覚えておいてください。

絵文字と多くの珍しい文字(𠮷 など)は BMP の外にあり、JavaScript では "😀".length === 2 です。 文字数で検証すると誤りやすくなります。

const text = "😀";
console.log(text.length);          // 2、UTF-16 コードユニット数
console.log([...text].length);     // 1、コードポイント数
console.log(Buffer.byteLength(text)); // 4、UTF-8 バイト数

ニックネームの長さ制限を .length で計算すると、絵文字一つが二枠を消費します。バックエンドがバイトで数えるなら四です。 定義を揃えるときは、コードポイント、コードユニット、バイトのどれを意味するかを明示してください。

3. 数値と特殊値

大きな整数 9007199254740993 が 9007199254740992 になったのはなぜか

JavaScript の JSON.parse が数値を IEEE 754 倍精度浮動小数点に変換し、安全な最大整数が 2^53-1 だからです。

Number.MAX_SAFE_INTEGER === 9007199254740991  // true
9007199254740993                              // 実際には 9007199254740992 になる

2^53-1 を超える数値は末尾の桁が丸められえます。典型的な被害者はスノーフレークアルゴリズムで生成された 19 桁のユーザー ID、 注文番号、ナノ秒タイムスタンプです。

三つの修正を優先順に:

  1. サーバーで ID を文字列としてシリアライズする:"id": "9007199254740993"。精度問題を根から除去します。
  2. 無損失のパースライブラリを使う:JSON.parse の代替は大きな整数をそのまま保てます。たとえば json-bigint。
  3. 表示用に正規表現で抽出する:文書全体をパースせず、対象フィールドをテキストとして一致させます。

JSON.stringify にも対称的な問題があることに注意してください。安全な整数範囲外の数値は、シリアライズ後に元の値を保てないことがあります。

NaN と Infinity は JSON に入れられるか

入れられません。JSON の数値生成規則は十進表記だけを許し、NaN、Infinity、-Infinity は正しい JSON 値ではありません。

JavaScript の方法は「静かな劣化」です。

JSON.stringify({ value: NaN });        // '{"value":null}'
JSON.stringify({ value: Infinity });   // '{"value":null}'

エラーなしで、NaN が単に null になります。「フロントエンドが NaN を送り、バックエンドが null を受け取った」古典的な場面です。 そのような意味論を伝える必要があるときは、文字列か合意したセンチネル値で明示的に表現します。

{ "ratio": null, "ratioState": "not-a-number" }

数値は 1e3 や 0.5 と書けるか

書けます。JSON の数値は小数と指数をサポートするので、1e3、1E3、0.5、-0.5 はすべて正しいです。

ただし制約があります。

  • . で始まったり終わったりできない:.5 と 5. はどちらも不正なので、0.5 と 5 と書く。
  • 先頭ゼロなし:01 は不正。
  • 16 進数なし:0x1F は不正。それは JSON5 の拡張です。
  • -0 は正しいが、JSON.stringify(-0) は 0 を出力し、符号を失う。

4. オブジェクトの意味論

重複キーはどうなるか

振る舞いは実装依存です。RFC 8259 はキー名が一意である「べき」とだけ述べ、重複時に何が起きるかを規定していません。

実際には大多数の実装が最後の値を取ります。

JSON.parse('{"a":1,"a":2}')  // { a: 2 }

しかし当てにできません。一部の Java ライブラリ、Go の encoding/json、Python の json はバージョン間で異なります。 最初の値を取るもの、エラーにするもの、最後を取るものがあります。重複キーをエラーとして扱うのが言語をまたぐ最も安全な方法です。

重複キーを目で見つけることはできません。圧縮された JSON では同じキーが数千文字離れていることがあります。スクリプトを使います。

# 同じ階層で複数回現れるキーを大まかに検査する
jq -r 'paths(scalars) | join(".")' data.json | sort | uniq -d

undefined はどうなるか

場合によります。これが最もバグを生みやすい箇所です。

JSON.stringify({ a: undefined });              // '{}'  キーが完全に落ちる
JSON.stringify({ a: undefined, b: 1 });        // '{"b":1}'
JSON.stringify([undefined]);                   // '[null]'  配列内では null になる
JSON.stringify({ a: function () {} });         // '{}'  関数も落ちる

同じ undefined が、オブジェクト内では「フィールドが消える」、配列内では「null になる」を意味します。 バックエンドが「フィールド欠落」と「フィールドが null」で異なる分岐を取るなら、ここで不整合が生まれます。

「値なし」を明示的に表現する必要があるときは、undefined ではなく null を使ってください。

{"__proto__": {...}} をパースするとどうなるか

JSON.parse 自体は安全です。__proto__ は普通の own プロパティとして扱われ、プロトタイプを変えません。

危険なのはパース後のマージ操作です。

const patch = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, patch);       // 一部の実装はプロトタイプを汚染する

正しい方法は own プロパティを反復し、__proto__、constructor、prototype をスキップすることです。 詳細は性能とセキュリティ FAQにあります。

5. クイックリファレンス

構文 正しいか 備考
{"a": 1,} いいえ 末尾カンマ
{'a': 1} いいえ 単一引用符
{a: 1} いいえ 引用符なしキー
{"a": 1} // comment いいえ コメント
"hello" はい トップレベル文字列は正しいが、API では稀
{"a": 1e3} はい 指数はサポート
{"a": 01} いいえ 先頭ゼロ
{"a": NaN} いいえ 不正なリテラル。stringify が null にする
\uFEFF{"a":1} いいえ BOM は JSON テキストの一部ではない
{"a":1,"a":2} 議論あり 実装は最後の値を取るが、当てにしない