2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptのシリアライズ設計:JSONの限界と構造保持のためのカスタム復元戦略

Posted at

概要

JavaScriptでデータを保存・送受信・復元する際に使われるのが シリアライズ(serialize)デシリアライズ(deserialize) である。
標準的には JSON.stringify() / JSON.parse() が使われるが、これは 万能ではない。

特に以下のような構造は、JSONでは正しく保存・復元できない:

  • Date オブジェクト → 文字列化される
  • Map, Set, RegExp → 通常のObjectになる
  • 関数 → 保存されない
  • クラスインスタンス → プロトタイプ消失
  • 循環参照 → TypeError

この記事では、JSONの限界を乗り越える設計パターンと代替手段、カスタム復元戦略を体系化する。


JSON.stringify の基本仕様

const obj = {
  name: 'Toto',
  created: new Date(),
  greet: () => 'hi',
};

console.log(JSON.stringify(obj));

出力例:

{"name":"Toto","created":"2025-03-30T12:00:00.000Z"}
  • ✅ Date → ISO8601文字列になる
  • ❌ 関数 → 無視される
  • ❌ プロトタイプ情報 → 消失

限界①:関数・クラスインスタンスが失われる

class User {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hi, ${this.name}`;
  }
}

const u = new User('Toto');
const json = JSON.stringify(u);
const parsed = JSON.parse(json);

parsed.greet(); // ❌ TypeError: parsed.greet is not a function

→ ✅ greet() メソッドはプロトタイプにあり、JSON化では失われる


限界②:循環参照は保存できない

const a = {};
a.self = a;
JSON.stringify(a); // ❌ TypeError: Converting circular structure to JSON

解決①:カスタム toJSON 実装による制御

class User {
  constructor(name) {
    this.name = name;
    this.created = new Date();
  }

  toJSON() {
    return {
      name: this.name,
      created: this.created.toISOString(),
      __type: 'User'
    };
  }
}

→ ✅ 特定の型としてシリアライズ可能


解決②:カスタム reviver による復元処理

const str = JSON.stringify(u);

const revived = JSON.parse(str, (key, value) => {
  if (value?.__type === 'User') {
    const user = new User(value.name);
    user.created = new Date(value.created);
    return user;
  }
  return value;
});

→ ✅ JSON.parse 第2引数で「復元処理」を実装可能
→ ✅ 型付きデータの保存→復元を安全に設計できる


解決③:構造保存向きの代替手段

ライブラリ / 手法 特徴
flatted 循環参照をサポートするJSON互換フォーマット
superjson Date, Map, Set, RegExpなどの復元に対応
structuredClone() DOM標準・複製のみ(永続化には使えない)
msgpack / protobuf バイナリフォーマット・構造保持に優れる

再設計の戦略:何を残し、何を捨てるか

✅ 状態だけ残す:

const snapshot = JSON.stringify(instance); // → DB保存 / API送信用

✅ 振る舞い(メソッド)も復元したい:

// toJSON / reviver または class構造 + factory関数

✅ 構造そのまま複製したい:

// structuredClone() or flatted

よくある落とし穴

❌ Dateをそのまま比較する

JSON.parse(json).created instanceof Date; // ❌ false

→ ✅ new Date(value) で復元が必要


❌ メソッドが復元されると思っている

→ メソッドは保存されない → prototype再構築が必要


設計判断フロー

① データだけ保存? → JSONで十分

② メソッドや型情報も保存? → toJSON + reviver

③ Map/Set/Date/RegExpも使う? → superjson or flatted

④ 循環参照あり? → flatted or custom traversal

⑤ バイナリで高速化したい? → msgpack / protobuf

結語

シリアライズは、単なる文字列変換ではない。
それは「どの情報を残し、どの構造を捨てるか」という設計的決断のプロセスである。

  • JSONは軽くて早いが、構造は失う
  • toJSON / reviver は保存と復元を設計に取り込む術
  • データの“意味”を維持するためには、保存形式の設計こそが最重要

構造を残すのか、値だけ残すのか――それを選ぶのは、設計者である。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?