コンテンツへスキップ
Toova
すべてのツール

時間を節約する 7 つの JSON テクニック

Toova

すべての JavaScript 開発者は週に何十回も JSON.parseJSON.stringify を使います。しかし、ほとんどの人は基本で止まります — API レスポンスのパースとオブジェクトの文字列へのシリアル化。API はそれ以上のものを提供し、あまり使われない機能のいくつかは、サードパーティのライブラリやデバッグの何時間も必要とする問題を解決します。

このガイドでは、デフォルトを超えた 7 つのテクニックを取り上げます。それぞれが実際のコードベースに即座に適用できます。抽象化なし、作り上げた例なし — これらは本番システムで実際に出てくるパターンです。

この記事のコード例はいずれも、JSON 全体をブラウザ内で検証して整形する Toova JSON フォーマッタを使って確認、探索できます。

1. JSON ラウンドトリップでのディープクローン (とその限界)

JavaScript の最も古いトリック: プレーンオブジェクトのディープクローンを作成するには JSON.parse(JSON.stringify(obj)) を使用します。

const original = { a: 1, b: { c: 2 } };

// 単純な方法 — 深くネストされたオブジェクトは依然として共有される
const shallowCopy = { ...original }; // b は同じオブジェクトを指している

// JSON ディープクローン — 完全に独立したコピーを作成
const deepClone = JSON.parse(JSON.stringify(original));

deepClone.b.c = 99;
console.log(original.b.c); // 2 — 元のオブジェクトは変更されない

これは文字列にシリアル化してパースバックすると、共有参照のない完全に新しいオブジェクトツリーを作成するからです。高速で、依存関係を必要とせず、ES5 から利用できます。

メモリ内使用のためのモダンな代替は structuredClone() です:

// モダンな代替: structuredClone() — より多くの型を処理
// Node.js 17+ およびすべてのエバーグリーンブラウザでサポート
const clone = structuredClone(original);

JSON アプローチに依存する前に、その限界を理解してください:

// JSON.parse/stringify が処理できないもの:
const broken = {
  date: new Date(),      // 文字列になる — Date プロトタイプを失う
  fn: () => 'hello',    // 黙って削除される
  undef: undefined,     // 黙って削除される
  inf: Infinity,        // null になる
  map: new Map(),       // {} になる
  cycle: null,          // 循環参照は例外を投げる
};

オブジェクトにプレーンデータのみが含まれている場合 — 文字列、数値、ブール値、配列、ネストされたプレーンオブジェクト — JSON ラウンドトリップは安全で高速です。それ以外のものには structuredClone() または専用ライブラリを優先してください。

2. フィルタリングとマスキングのためのカスタム replacer

ほとんどの開発者は JSON.stringify が 2 番目の引数を取ることを知っていますが、あまり使いません。その 2 番目の引数が replacer です: 含めるキーの配列、または各値のシリアル化方法を正確に制御する関数のいずれかです。

配列の replacer — 特定のキーをホワイトリスト化:

const user = {
  id: 'u_001',
  name: 'Alice',
  password: 'hunter2',        // ログに表示してはいけない
  creditCard: '4111111111111111', // ログに表示してはいけない
  role: 'admin',
};

// 配列としての replacer: これらのキーのみを含める
JSON.stringify(user, ['id', 'name', 'role']);
// '{"id":"u_001","name":"Alice","role":"admin"}'

関数の replacer — 値を変換または編集:

// 関数としての replacer: キー/値を完全に制御
const masked = JSON.stringify(user, (key, value) => {
  if (key === 'password' || key === 'creditCard') return '[REDACTED]';
  if (key === '' ) return value; // ルートオブジェクト — 常に返す
  return value;
});
// '{"id":"u_001","name":"Alice","password":"[REDACTED]","creditCard":"[REDACTED]","role":"admin"}'

より高度なバージョンは、キー名ではなく形状に基づいて値をマスクします:

// 型ベースのマスキング用 replacer
const sanitize = (key, value) => {
  if (typeof value === 'string' && value.match(/^4[0-9]{15}$/)) {
    return '****-****-****-' + value.slice(-4);
  }
  return value;
};

このテクニックはロギングミドルウェアに不可欠です: 完全なオブジェクトコンテキストの構造化ログが必要ですが、特定のフィールドはログ集約に決して到達してはなりません。replacer により、コードベース全体に編集ロジックを散らすのではなく、シリアル化境界でこれを処理できます。

3. キーを決定論的にソートする

JavaScript のオブジェクトキー順は挿入順(文字列キーの場合)です。同じキーを持つが異なる順で作成された 2 つのオブジェクトは、異なる JSON 文字列を生成し、単純な等価チェック、キャッシュキー、コンテンツハッシュを壊します。

function sortedStringify(obj) {
  return JSON.stringify(obj, Object.keys(obj).sort());
}

const a = { z: 1, a: 2, m: 3 };
const b = { a: 2, m: 3, z: 1 };

sortedStringify(a) === sortedStringify(b); // true — キー順が正規化される

深くネストされたオブジェクトの場合、ソートを再帰的に適用します:

// ネストされたオブジェクトのための再帰的キーソート
function sortKeys(value) {
  if (Array.isArray(value)) return value.map(sortKeys);
  if (value !== null && typeof value === 'object') {
    return Object.fromEntries(
      Object.keys(value).sort().map((k) => [k, sortKeys(value[k])])
    );
  }
  return value;
}

const sorted = JSON.stringify(sortKeys(deepNested));

ソートされた JSON は以下の場合に不可欠です:

  • リクエストボディからキャッシュキーを生成する
  • JSON ペイロード上でチェックサムや署名を計算する
  • フィールド順に関係なくテストで API レスポンスを比較する
  • 順序が等価性に影響しないべき設定オブジェクトを保存する

ソートと整形の後、テキスト差分ツールを使って 2 つの正規化された JSON 文字列を比較し、バージョン間でどの値が変更されたかを正確に確認します。

4. 精度を失わない BigInt の処理

JavaScript の Number 型は、253 − 1 までの整数を安全に表現できます。分散システムで生成された ID、マイナー単位の金融金額、ナノ秒のタイムスタンプには十分ではありません。BigInt は任意精度整数をカバーしますが、JSON.stringify はそれをどう処理すべきかを知りません。

const data = {
  amount: 9007199254740993n, // Number.MAX_SAFE_INTEGER より大きい
};

// これは例外を投げる: TypeError: Do not know how to serialize a BigInt
JSON.stringify(data); // エラー

標準的な回避策:

// 解決策 1: replacer で文字列に変換
JSON.stringify(data, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"amount":"9007199254740993"}'

// 解決策 2: BigInt プロトタイプの toJSON() (モンキーパッチ — 注意して使う)
BigInt.prototype.toJSON = function () { return this.toString(); };
JSON.stringify(data); // '{"amount":"9007199254740993"}'

パース側では、reviver 関数が文字列表現から BigInt 値を復元できます:

// パース時に BigInt を復元する reviver
const revived = JSON.parse('{"amount":"9007199254740993"}', (key, value) => {
  if (key === 'amount') return BigInt(value);
  return value;
});
console.log(typeof revived.amount); // 'bigint'

BigInt から文字列への変換を共有シリアル化レイヤーに保持して、一貫して適用されるようにします。コードベースの端で BigInt がアドホックな JSON.stringify 呼び出しに漏れると、追跡しにくい予測不可能なエラーが発生します。

5. 循環参照の検出

循環参照は、オブジェクトがそれ自体への参照やオブジェクトグラフ内の祖先への参照を含むときに発生します。思っているよりも一般的です: イベントエミッタ、DOM ノード、React ファイバーノード、ORM エンティティはすべて頻繁に逆参照を持ちます。

// 循環参照の例
const obj = { name: 'node' };
obj.self = obj; // obj が自分自身を参照

JSON.stringify(obj); // 例外を投げる: TypeError: Converting circular structure to JSON

訪問したオブジェクトを追跡するカスタム replacer で処理します:

// 手動の循環参照ハンドラ
function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  });
}

safeStringify(obj); // '{"name":"node","self":"[Circular]"}'

WeakSet はここで適切なデータ構造です: ガベージコレクションを妨げずにオブジェクト参照を保持し、ルックアップは O(1) です。このパターンは、シリアル化中に例外を投げる代わりにエラーが優雅に低下することを望むロギングミドルウェアでも機能します。

一般的なバリエーション: 循環参照を [Circular] としてマークする代わりに、[Circular: $.config.parent] のようなパス文字列に置き換えて、デバッグ中に参照位置を明示します。

6. 配列を 1 行で整形する

JSON.stringify の 3 番目の引数はインデントです。2 または 4 を渡すと、すべてが複数行に展開されます。これはオブジェクトには素晴らしいですが、タグ、ID、座標のようなプリミティブの配列には冗長になることがあります。

const mixed = {
  title: 'Report',
  tags: ['json', 'api', 'debug'],
  config: { indent: 2, sortKeys: true },
  count: 42,
};

// デフォルト: すべて複数行
JSON.stringify(mixed, null, 2);
// {
//   "title": "Report",
//   "tags": [
//     "json",
//     "api",
//     "debug"
//   ],
//   "config": {
//     "indent": 2,
//     "sortKeys": true
//   },
//   "count": 42
// }

出力を後処理して単純な配列を 1 行に折りたたむことができます:

// 単一行の配列のためのカスタム replacer
function prettyMixed(obj) {
  const raw = JSON.stringify(obj, null, 2);
  // プリミティブのみの配列を 1 行に折りたたむ
  return raw.replace(
    /\[\n\s+([\s\S]*?)\n\s+\]/g,
    (match, inner) => {
      const items = inner.split(',\n').map((s) => s.trim());
      if (items.every((s) => !/^[{\[]/.test(s))) {
        return '[' + items.join(', ') + ']';
      }
      return match;
    }
  );
}

結果: オブジェクトは可読性のために複数行のままで、プリミティブの配列はコンパクトさのために 1 行に折りたたまれます。これは、人間と機械の両方が同じデータを読む必要がある多くの設定ファイルと構造化ログ出力で使用される形式です。JSON フォーマッタJSON to YAML 変換ツールはこの折りたたみを自動的に処理します。

7. 大きな JSON のストリーミング

パースする前に大きな JSON ファイルを完全にメモリにロードすることは、JSON 処理パイプラインで最も一般的なパフォーマンスミスです。100 MB の JSON レスポンスは、生の文字列のために少なくとも 100 MB のヒープを割り当て、その後、パースされたオブジェクトのために 2 番目のアロケーションを行います。数メガバイトを超えるファイルの場合、ストリーミングパーサーがデータを段階的に処理します。

ストリーミングライブラリを使った Node.js の場合:

// node --experimental-vm-modules (またはストリーミング JSON ライブラリを使用)
// JSON Source Map 提案によるネイティブストリーミング (Stage 3, 2026):

// 当面は @streamparser/json や jsonstream2 のようなライブラリを使用
import { createReadStream } from 'fs';
import parser from '@streamparser/json';

const jsonParser = new parser.JSONParser();
jsonParser.onValue = ({ value, key, parent }) => {
  if (key === 'id') {
    console.log('id を発見:', value);
  }
};

createReadStream('large.json').pipe(jsonParser);

Fetch Streams API を使ったブラウザの場合:

// Fetch + JSON ストリームデコーダによるブラウザ側ストリーミング:
const response = await fetch('/api/large-data');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  // バッファから完成した JSON オブジェクトを処理...
}

ストリーミングは、バッチ処理パイプライン、大規模なエクスポートエンドポイント、ログ解析スクリプトで最も役立ちます。1 MB 未満の典型的な API レスポンスでは、標準の JSON.parse は十分に高速で、はるかにシンプルです。ストリーミングが実際に効果を発揮するしきい値は、パースされた構造の複雑さとターゲット環境のヒープサイズに応じて、約 5〜10 MB です。

ストリーミングパーサーを構築する前に大きな JSON レスポンスの構造を探索するには、サンプルを JSON フォーマッタに貼り付けて、必要なデータを含むパスを確認し、その後、さらなる解析のために CSV または YAML に変換します。

まとめ

これら 7 つのテクニックは、JSON API の最も影響力のあるコーナーをカバーします:

  • ディープクローン(ラウンドトリップ)— プレーンデータには高速、限界を理解する
  • replacer — シリアル化境界でキーをフィルタリングし、機密値をマスク
  • キーソート — キャッシュキー、署名、テストアサーションのための決定論的出力
  • BigInt 処理 — 文字列にシリアル化し、パース時に復元
  • 循環参照の検出 — 安全なシリアル化のための WeakSet ベースの replacer
  • コンパクト配列での整形 — 過剰な空白なしで人間が読める出力
  • 大きな JSON のストリーミング — 大きなファイルや API レスポンスのための段階的パース

JSON.stringify の replacer と space 引数の完全なドキュメントについては、MDN JSON.stringify リファレンスを参照してください。モダンなディープクローン代替については、サポートされるすべての型とエッジケースをカバーする structuredClone() ドキュメントを参照してください。