JSONの整形係を作りました。担当は改行と字下げです。
ところが、数字まで丸くして返してきました。
人間関係ならありがたい配慮ですが、IDには角が立っていてもらわないと困ります。
9007199254740993 を渡したら、戻ってきたのは 9007199254740992。頼んだ字下げに、頼んでいない値下げが付いてきます。
個人で公開している After 6 ToolboxのJSON整形・圧縮 で、以前の実装にあった問題です。合成データで再現し、2026年10月22日に修正しました。
原因を追うと、整形係に余計な仕事まで任せていました。今回は、その担当業務を見直した話です。
整形係の業務マニュアル
以前の処理は、よく見るこの形です。JSON.parse() で読み、JSON.stringify() でインデント付きの文字列に戻します。
const input = '{"id":9007199254740993,"decimal":1.2300,"zero":-0,"exp":1e+400}';
console.log(JSON.stringify(JSON.parse(input), null, 2));
Node.jsで実行すると、こうなります。
{
"id": 9007199254740992,
"decimal": 1.23,
"zero": 0,
"exp": null
}
IDだけでなく、小数の末尾のゼロも片づけられ、巨大な数は null になりました。整理整頓を頼んだら、かなり踏み込んだ断捨離が始まっています。
JSON.parse() は、JSONの数値をJavaScriptの Number に変換します。安全に整数を扱える上限は 9007199254740991。それを超える整数には、正確に表現できないものがあります。
さらに JSON.stringify() は、変換後の値をJSONとして書き直します。この往復で、次のことが起きます。
| 元の入力 | 往復後 | 起きたこと |
|---|---|---|
9007199254740993 |
9007199254740992 |
整数の精度が失われる |
1.2300 |
1.23 |
値は同じでも、末尾のゼロが消える |
-0 |
0 |
負のゼロの表記が消える |
1e+400 |
null |
Number では Infinity となり、再出力で null になる |
全部を「丸め誤差」で片づけると説明を間違えます。値が変わる問題と、表記が変わる問題が混ざっています。
整形ツールとしては、どちらも勝手に変えたくありません。しかし、実装では「JavaScriptの値に変換してから、書き直す」という手順にしていました。
担当者が指示どおりに働いた結果でした。責任者の席が近くなってきました。
書類の中身まで整理しないでください
修正後も、構文の検査には JSON.parse() を使っています。ただし、返ってきたオブジェクトを出力の材料にしません。
元の入力から文字列・数値・括弧などのトークンを取り出し、文字列の外側にある空白と改行だけを整えます。
圧縮部分を抜き出すと、考え方はこれだけです。
function compactJson(input) {
JSON.parse(input); // 構文検査。変換結果は出力に使わない
const tokens = input.match(/"(?:\\[\s\S]|[^"\\])*"|[{}\[\],:]|[^\s{}\[\],:]+/g) ?? [];
return tokens.join('');
}
console.log(compactJson('{ "name": "山 田", "id": 9007199254740993 }'));
// {"name":"山 田","id":9007199254740993}
この正規表現だけでJSONの正しさを判定しているわけではありません。先に構文検査を通すのが前提です。
ここで単純にすべての空白を削ると、"山 田" が "山田" になります。数字の次は名前です。担当業務がまた広がってしまいます。
文字列をひとまとまりで取り出せば、名前の空白やエスケープもそのまま残せます。整形時は、同じトークン列の括弧やカンマに合わせて改行とインデントを足します。
この方式で、大きな整数、小数の末尾ゼロ、指数表記、負のゼロを保持するようにしました。キーの並びや重複キーも保持します。重複キーをどう扱うかといった意味上の判断も、今回の整形処理の担当から外しています。
今度は空白が働きすぎる
担当を字下げに絞っても、考えることは残ります。
配列が深く入れ子になっていると、整形で足すインデントだけでも出力が大きくなります。原稿を見やすくするために用意した余白が、原稿より幅を利かせます。頼んだ仕事には熱心です。
現行実装では、入力を20万UTF-16コード単位、整形後の出力を200万UTF-16コード単位までに制限しています。深い入れ子は再帰を使わずに処理し、出力上限を超えたら中断して、圧縮するか入力を小さくするよう案内します。
エラー時には前の結果を消し、コピーも無効にします。今回の処理は失敗しているのに、机の上には前の完成品がある。その状態でコピーできると、どの入力の結果を持ち帰るのか分からなくなるためです。
大きな整数や文字列の保持に加え、不正な構文、深い配列、出力上限を確認しました。公開画面でも、大きなIDの整形・圧縮と、不正入力時に結果が消えることを確かめています。
参考資料と補足
実装・検証・執筆には生成AIを使っています。掲載例は合成データです。再現コードはNode.js v24.22.1で確認しています。
担当業務は見直しました
今回の修正では、構文検査と、出力する文字の扱いを分けました。構文の正しさは JSON.parse() で確かめ、整形結果は元のトークンから作ります。
見た目を整えてほしい担当に、値の解釈と書き直しまで頼んでいた。最初に決めるべきだったのは、何を変えてよいかでした。
これで整形係は、数字に気を回さず、改行と字下げに専念できます。
担当業務は見直しました。責任者は続投です。
個人開発なので、交代要員がいません。