json-trailing-comma

このページは FormatArc のブログ記事が引用している実測の生データです。検索結果には表示されません (noindex)。

Files

FileSize
README.md4.1 KB
cases.json964 B
measure.mjs6.4 KB
package-lock.json985 B
package.json236 B
python-runner.py771 B
results.json9.0 KB

README.md

# JSON trailing comma acceptance and error messages (5 parsers, measured)

`json-trailing-comma` 記事が引用するパーサー別エラーメッセージ・行番号の一次ソース。
末尾カンマ (trailing comma) を含む JSON を主要 5 パーサーに実際に食わせ、**受理するか
どうか・実際に出るエラーメッセージ文字列・報告される行/列/offset** を記録する。

## 測定対象パーサー

| パーサー | 呼び方 | バージョン |
| --- | --- | --- |
| V8 (`JSON.parse`) | Node.js 組み込み | Node 実行系のバージョンに追従 (measure.mjs 内で `process.version` を記録) |
| Python (`json.loads`) | `python3 python-runner.py` をサブプロセスで呼ぶ | `python3 --version` を記録 |
| `jq` | インストールされていれば `jq .` にケースを標準入力で渡す | `jq --version` を記録。無ければ `available: false` |
| JSON5 (`json5` npm package) | `JSON5.parse` | package.json 固定 (2.2.3) |
| JSONC (`jsonc-parser` npm package, VS Code が使うパーサー) | `jsonc.parse` + `errors` 配列 | package.json 固定 (3.3.1) |

V8 は FormatArc の json-formatter ツールが実際に使っているパーサー (`JSON.parse` / `JSON.stringify`)。

## ケース (cases.json)

1. `object-trailing` — オブジェクトの最後のメンバーの後の末尾カンマ
2. `array-trailing` — 配列の最後の要素の後の末尾カンマ
3. `nested-trailing` — ネストした配列内の末尾カンマ (外側は正しいカンマ)
4. `multiline-object-trailing` — 複数行オブジェクトでの末尾カンマ (行番号が意味を持つケース)
5. `double-trailing` — カンマが2つ連続する壊れ方 (`[1, 2,,]`)
6. `valid-baseline` — 末尾カンマ無し。全パーサーが受理することを確認する陰性対照

## `accepted` の定義

- V8 / Python / jq / JSON5 は「例外を投げずにパースが完了したか」で判定する。
- `jsonc-parser` の `parse()` はエラー回復 (lenient recovery) を行う実装で、末尾カンマが
  あっても**例外を投げず値を返す**。そのため本ベンチでは `accepted = (errors.length === 0)`
  と定義し、`errors` 配列 (`jsonc.printParseErrorCode` で文字列化したもの) をそのまま
  `errors` フィールドに残す。値が返ってきたかどうかとは別軸であることに注意。

## 再現手順

```bash
cd scripts/benchmarks/json-trailing-comma
npm install
node measure.mjs   # Python 側は python-runner.py をサブプロセスで自動的に呼ぶ
```

`results.json` が上書き生成される。標準出力にも同じ JSON が出る。

## 測定のスコープと限界

- **バージョン依存**: 各パーサーの挙動・エラーメッセージ文言はバージョンで変わりうる。
  `results.json` の `environment` に記録した実測時のバージョンでのみ保証する
  (measure.mjs 実行時点で固定されている `json5` / `jsonc-parser` を除き、V8 / Python / jq
  は実行系にインストールされているものをそのまま使う)。
- **jq 未インストール環境**: `environment.jqAvailable` が `false` になり、各ケースの
  jq 側エントリは `{ "available": false, "accepted": null, "message": null, "position": null }`
  で記録される。推測で埋めない。
- **jsonc-parser の `accepted`**: 上記の通り「エラー配列が空かどうか」であり、値が
  返ってきたかどうかとは別。記事本文で引用する際は取り違えないこと。
- **位置情報の形式が揃わない**: V8 は `position` (offset) と `line/column` の両方または
  片方のみを返す (ケースによって message の書式自体が変わる)。jq / Python は
  line/column、jsonc-parser は offset のみで line/column を返さない。フォーマット横断の
  「行番号」比較には JSON5/V8/Python/jq いずれもエラーメッセージ全体を引用するのが安全。

記事本文の表は `results.json` の値と完全一致させること (数値・文言の捏造禁止ルール準拠)。

results.json

{
  "generatedAt": "2026-08-17T01:48:24.055Z",
  "environment": {
    "node": "v26.7.0",
    "python": "3.14.6",
    "jq": "1.7.1-apple",
    "jqAvailable": true,
    "json5": "2.2.3",
    "jsoncParser": "3.3.1",
    "os": "Darwin 25.5.0",
    "machine": "Apple M5 Pro"
  },
  "cases": [
    {
      "id": "object-trailing",
      "description": "Trailing comma after the last member of an object",
      "input": "{\"a\": 1, \"b\": 2,}",
      "parsers": {
        "V8 (Node.js v26.7.0) JSON.parse": {
          "accepted": false,
          "message": "Expected double-quoted property name in JSON at position 16 (line 1 column 17)",
          "position": {
            "offset": 16,
            "line": 1,
            "column": 17
          }
        },
        "Python 3.14.6 json.loads": {
          "accepted": false,
          "message": "Illegal trailing comma before end of object: line 1 column 16 (char 15)",
          "position": {
            "line": 1,
            "column": 16,
            "offset": 15
          }
        },
        "jq jq-1.7.1-apple": {
          "accepted": false,
          "message": "jq: parse error: Expected another key-value pair at line 1, column 17",
          "position": {
            "line": 1,
            "column": 17,
            "offset": null
          }
        },
        "json5 2.2.3": {
          "accepted": true,
          "message": null,
          "position": null
        },
        "jsonc-parser 3.3.1": {
          "accepted": false,
          "message": "PropertyNameExpected at offset 16; ValueExpected at offset 16",
          "position": {
            "offset": 16,
            "line": null,
            "column": null
          },
          "errors": [
            {
              "error": "PropertyNameExpected",
              "offset": 16,
              "length": 1
            },
            {
              "error": "ValueExpected",
              "offset": 16,
              "length": 1
            }
          ]
        }
      }
    },
    {
      "id": "array-trailing",
      "description": "Trailing comma after the last element of an array",
      "input": "[1, 2, 3,]",
      "parsers": {
        "V8 (Node.js v26.7.0) JSON.parse": {
          "accepted": false,
          "message": "Unexpected token ']', \"[1, 2, 3,]\" is not valid JSON",
          "position": null
        },
        "Python 3.14.6 json.loads": {
          "accepted": false,
          "message": "Illegal trailing comma before end of array: line 1 column 9 (char 8)",
          "position": {
            "line": 1,
            "column": 9,
            "offset": 8
          }
        },
        "jq jq-1.7.1-apple": {
          "accepted": false,
          "message": "jq: parse error: Expected another array element at line 1, column 10",
          "position": {
            "line": 1,
            "column": 10,
            "offset": null
          }
        },
        "json5 2.2.3": {
          "accepted": true,
          "message": null,
          "position": null
        },
        "jsonc-parser 3.3.1": {
          "accepted": false,
          "message": "ValueExpected at offset 9",
          "position": {
            "offset": 9,
            "line": null,
            "column": null
          },
          "errors": [
            {
              "error": "ValueExpected",
              "offset": 9,
              "length": 1
            }
          ]
        }
      }
    },
    {
      "id": "nested-trailing",
      "description": "Trailing comma inside a nested array, valid comma at the outer level",
      "input": "{\"a\": [1, 2,], \"b\": 3}",
      "parsers": {
        "V8 (Node.js v26.7.0) JSON.parse": {
          "accepted": false,
          "message": "Unexpected token ']', ...\"a\": [1, 2,], \"b\": 3}\" is not valid JSON",
          "position": null
        },
        "Python 3.14.6 json.loads": {
          "accepted": false,
          "message": "Illegal trailing comma before end of array: line 1 column 12 (char 11)",
          "position": {
            "line": 1,
            "column": 12,
            "offset": 11
          }
        },
        "jq jq-1.7.1-apple": {
          "accepted": false,
          "message": "jq: parse error: Expected another array element at line 1, column 13",
          "position": {
            "line": 1,
            "column": 13,
            "offset": null
          }
        },
        "json5 2.2.3": {
          "accepted": true,
          "message": null,
          "position": null
        },
        "jsonc-parser 3.3.1": {
          "accepted": false,
          "message": "ValueExpected at offset 12",
          "position": {
            "offset": 12,
            "line": null,
            "column": null
          },
          "errors": [
            {
              "error": "ValueExpected",
              "offset": 12,
              "length": 1
            }
          ]
        }
      }
    },
    {
      "id": "multiline-object-trailing",
      "description": "Trailing comma after the last property in a multi-line object, so line numbers are meaningful",
      "input": "{\n  \"a\": 1,\n  \"b\": 2,\n}",
      "parsers": {
        "V8 (Node.js v26.7.0) JSON.parse": {
          "accepted": false,
          "message": "Expected double-quoted property name in JSON at position 22 (line 4 column 1)",
          "position": {
            "offset": 22,
            "line": 4,
            "column": 1
          }
        },
        "Python 3.14.6 json.loads": {
          "accepted": false,
          "message": "Illegal trailing comma before end of object: line 3 column 9 (char 20)",
          "position": {
            "line": 3,
            "column": 9,
            "offset": 20
          }
        },
        "jq jq-1.7.1-apple": {
          "accepted": false,
          "message": "jq: parse error: Expected another key-value pair at line 4, column 1",
          "position": {
            "line": 4,
            "column": 1,
            "offset": null
          }
        },
        "json5 2.2.3": {
          "accepted": true,
          "message": null,
          "position": null
        },
        "jsonc-parser 3.3.1": {
          "accepted": false,
          "message": "PropertyNameExpected at offset 22; ValueExpected at offset 22",
          "position": {
            "offset": 22,
            "line": null,
            "column": null
          },
          "errors": [
            {
              "error": "PropertyNameExpected",
              "offset": 22,
              "length": 1
            },
            {
              "error": "ValueExpected",
              "offset": 22,
              "length": 1
            }
          ]
        }
      }
    },
    {
      "id": "double-trailing",
      "description": "Two consecutive commas before the closing bracket",
      "input": "[1, 2,,]",
      "parsers": {
        "V8 (Node.js v26.7.0) JSON.parse": {
          "accepted": false,
          "message": "Unexpected token ',', \"[1, 2,,]\" is not valid JSON",
          "position": null
        },
        "Python 3.14.6 json.loads": {
          "accepted": false,
          "message": "Expecting value: line 1 column 7 (char 6)",
          "position": {
            "line": 1,
            "column": 7,
            "offset": 6
          }
        },
        "jq jq-1.7.1-apple": {
          "accepted": false,
          "message": "jq: parse error: Expected value before ',' at line 1, column 7",
          "position": {
            "line": 1,
            "column": 7,
            "offset": null
          }
        },
        "json5 2.2.3": {
          "accepted": false,
          "message": "JSON5: invalid character ',' at 1:7",
          "position": {
            "line": 1,
            "column": 7,
            "offset": null
          }
        },
        "jsonc-parser 3.3.1": {
          "accepted": false,
          "message": "ValueExpected at offset 6; ValueExpected at offset 7",
          "position": {
            "offset": 6,
            "line": null,
            "column": null
          },
          "errors": [
            {
              "error": "ValueExpected",
              "offset": 6,
              "length": 1
            },
            {
              "error": "ValueExpected",
              "offset": 7,
              "length": 1
            }
          ]
        }
      }
    },
    {
      "id": "valid-baseline",
      "description": "No trailing comma — negative control that every parser must accept",
      "input": "{\"a\": 1, \"b\": 2}",
      "parsers": {
        "V8 (Node.js v26.7.0) JSON.parse": {
          "accepted": true,
          "message": null,
          "position": null
        },
        "Python 3.14.6 json.loads": {
          "accepted": true,
          "message": null,
          "position": null
        },
        "jq jq-1.7.1-apple": {
          "accepted": true,
          "message": null,
          "position": null
        },
        "json5 2.2.3": {
          "accepted": true,
          "message": null,
          "position": null
        },
        "jsonc-parser 3.3.1": {
          "accepted": true,
          "message": null,
          "position": null
        }
      }
    }
  ]
}