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

要点:プロセス境界を越えるものには、素の 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 はすべて正しいです。
ただし実務では、トップレベルにオブジェクトか配列だけを使うことを推奨します。理由は二つ。
- 互換性:古いパーサー(RFC 4627 時代)はトップレベルがオブジェクトか配列であることを要求します。
- セキュリティ:トップレベル配列は歴史的に 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、 注文番号、ナノ秒タイムスタンプです。
三つの修正を優先順に:
- サーバーで ID を文字列としてシリアライズする:
"id": "9007199254740993"。精度問題を根から除去します。 - 無損失のパースライブラリを使う:
JSON.parseの代替は大きな整数をそのまま保てます。たとえばjson-bigint。 - 表示用に正規表現で抽出する:文書全体をパースせず、対象フィールドをテキストとして一致させます。
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} |
議論あり | 実装は最後の値を取るが、当てにしない |