【JS応用】オブジェクトを JSON に変換して戻す

オブジェクトを JSON に変換して戻す:フロントエンドにおけるシリアライズとデシリアライズの完全ガイド

フロントエンド開発において、JavaScriptのオブジェクトとJSON(JavaScript Object Notation)の相互変換は、避けて通ることのできない極めて重要な処理です。APIとの通信、ローカルストレージへの保存、あるいはWeb Workerとのデータ受け渡しなど、JSONは現代のWebアプリケーションを支えるデータ交換の標準フォーマットです。しかし、単に `JSON.stringify` と `JSON.parse` を使うだけでは、複雑なアプリケーションの要件を満たすことはできません。本記事では、この変換プロセスにおける技術的な深淵と、実務で直面する課題に対するベストプラクティスを徹底的に解説します。

JSON変換の基本メカニズムと制約

JavaScriptにおけるJSON変換は、`JSON.stringify()`(シリアライズ)と `JSON.parse()`(デシリアライズ)という2つの主要な静的メソッドによって行われます。

シリアライズとは、JavaScriptのメモリ上に存在するオブジェクトを、ネットワーク経由やファイル保存が可能な文字列形式に変換するプロセスです。一方、デシリアライズはその文字列を再びJavaScriptのオブジェクトへと復元するプロセスです。

しかし、JSONフォーマットには厳格な制約が存在します。JSONはあくまでデータ記述言語であり、JavaScriptのすべての型を表現できるわけではありません。以下の型は、標準的な変換プロセスで予期せぬ挙動を示します。

1. 関数(Function):`stringify` 時に無視されます。
2. `undefined`:オブジェクトのプロパティ値である場合は削除され、配列内の場合は `null` に変換されます。
3. シンボル(Symbol):無視されます。
4. `Date` オブジェクト:ISO 8601形式の文字列に変換されますが、`parse` しても文字列のまま残り、Date型には自動復元されません。
5. 循環参照(Circular Reference):オブジェクトが自分自身を参照している場合、`stringify` は例外(TypeError)をスローします。
6. `Map` や `Set`:空のオブジェクト `{}` としてシリアライズされます。

これらの制約を理解することは、堅牢なデータ永続化を実装する上での第一歩です。

ReplacerとReviverによる詳細制御

`JSON.stringify` と `JSON.parse` には、変換プロセスを細かく制御するための第2引数が用意されています。

`JSON.stringify(value, replacer, space)` における `replacer` は、変換される値をフィルタリングしたり、変換したりするための関数または配列です。特定のプロパティだけを除外したい場合や、特殊な型を文字列として変換したい場合に極めて有効です。

同様に、`JSON.parse(text, reviver)` における `reviver` は、パース後の各値に対して呼び出される関数です。これを利用することで、シリアライズ時に文字列化してしまった日付情報を、デシリアライズ時に自動的に `Date` オブジェクトへと再構築することが可能です。


const data = {
  name: "Project Alpha",
  createdAt: new Date()
};

// シリアライズ: Dateをそのまま保持する
const jsonString = JSON.stringify(data, (key, value) => {
  if (value instanceof Date) {
    return { __type: 'Date', value: value.toISOString() };
  }
  return value;
});

// デシリアライズ: Dateを復元する
const parsedData = JSON.parse(jsonString, (key, value) => {
  if (value && typeof value === 'object' && value.__type === 'Date') {
    return new Date(value.value);
  }
  return value;
});

console.log(parsedData.createdAt instanceof Date); // true

パフォーマンスと大規模データの扱い

フロントエンドにおいて、数MBを超える巨大なJSONデータをパースすることは、メインスレッドをブロックし、UIのフリーズ(Jank)を引き起こす原因となります。`JSON.parse` は同期的に実行されるため、巨大なJSONを扱う際には注意が必要です。

対策として、以下の手法が推奨されます。

1. Web Workersの活用:メインスレッドからパース処理を切り離し、バックグラウンドで実行します。
2. ストリーミングパース:`JSONStream` のようなライブラリを利用し、一度にすべてをパースするのではなく、断片ごとに処理を行います。
3. データの正規化:JSONの構造をフラットに保つことで、パース後の再構築コストを下げます。

特にモバイル端末ではCPUリソースが限られているため、巨大なデータ構造を扱う際は「必要な時に必要な分だけパースする」という遅延評価の考え方が不可欠です。

循環参照と複雑なオブジェクトのシリアライズ

実務において、State管理ライブラリ(ReduxやZustandなど)を扱う場合、オブジェクトが循環参照を持つことは珍しくありません。標準の `JSON.stringify` ではこれを解決できないため、専用のライブラリや手法が必要です。

最も一般的な解決策は、`flatted` などのライブラリを使用することです。これらは循環参照を検出し、インデックスベースの参照に置き換えることで、JSONとして出力可能にします。


// flattedを使用した例
import { stringify, parse } from 'flatted';

const obj = { name: 'Circular' };
obj.self = obj; // 循環参照

const serialized = stringify(obj);
const deserialized = parse(serialized);

console.log(deserialized.self === deserialized); // true

実務におけるエラーハンドリングの重要性

`JSON.parse` は、不正なJSON文字列を渡すと例外をスローします。APIから返却されるデータが常に正しいJSONであるという保証はどこにもありません。したがって、パース処理は必ず `try-catch` ブロックで囲む必要があります。

また、TypeScriptを使用している場合、`JSON.parse` は `any` 型を返すため、型の安全性が保証されません。パース直後に、`Zod` や `io-ts` のようなバリデーションライブラリを使用して、データのスキーマが期待通りであることを確認するのが、現代のフロントエンド開発における「ゴールデン・スタンダード」です。


import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string()
});

function safeParseUser(jsonString: string) {
  try {
    const raw = JSON.parse(jsonString);
    return UserSchema.parse(raw); // 型安全なパース
  } catch (e) {
    console.error("Invalid JSON format", e);
    return null;
  }
}

実務アドバイス:なぜ「生のJSON」をそのまま扱うべきではないのか

実務経験から得られる最も重要な教訓は、「JSONをそのままアプリケーションの内部状態として使用しない」ということです。

APIから取得したJSONは「外部データ」であり、アプリケーションの内部ロジックで使う「ドメインモデル」とは切り離して考えるべきです。JSONから受け取った直後に、バリデーションと正規化(必要であればクラスインスタンスへの変換)を行い、アプリケーションの内部形式に変換する「アダプター層」を設けることを推奨します。

これにより、APIのレスポンス構造が将来変更されたとしても、アプリケーション内部のロジックを修正する必要がなくなり、メンテナンス性が飛躍的に向上します。また、型定義を一箇所に集約することで、型安全性の維持も容易になります。

まとめ

オブジェクトのJSON変換は、単なるユーティリティ関数の呼び出しではありません。それは、外部の世界とアプリケーション内部をつなぐ「境界線」での重要な処理です。

1. 標準的な `JSON.stringify/parse` の制約(型、循環参照)を正しく理解すること。
2. `replacer` と `reviver` を駆使して、必要なデータ構造を制御すること。
3. 大規模なパースにはWeb Workerを活用し、UIスレッドを保護すること。
4. パース結果には必ずバリデーション(Zod等)を適用し、型安全を担保すること。
5. JSONを直接ビジネスロジックに流し込まず、アダプター層を介して内部モデルに変換すること。

これらの技術を習得し、堅牢なデータハンドリングを実装することで、あなたのフロントエンドアプリケーションはより安定し、拡張性の高いものとなるはずです。技術の細部にこだわり、データ構造の整合性を守り続けることこそが、プロフェッショナルなフロントエンドエンジニアの矜持と言えるでしょう。

コメント

タイトルとURLをコピーしました