flatted vs json-stringify-safe vs circular-json vs json-stringify-deterministic
JavaScript における循環参照と JSON 文字列化の戦略的選択
flattedjson-stringify-safecircular-jsonjson-stringify-deterministic類似パッケージ:

JavaScript における循環参照と JSON 文字列化の戦略的選択

circular-jsonflattedjson-stringify-deterministicjson-stringify-safe は、すべて JavaScript の JSON.stringify が抱える課題を解決するためのユーティリティですが、それぞれ異なる問題に焦点を当てています。circular-jsonflatted は循環参照(Circular Reference)を含むオブジェクトを完全にシリアライズ・デシリアライズすることを目的としていますが、circular-json は現在非推奨であり flatted への移行が推奨されています。一方、json-stringify-safe は循環参照 encountered 時にクラッシュせず安全に文字列化することを優先し、json-stringify-deterministic はオブジェクトのキー順序を固定してハッシュ生成やキャッシュ比較に適した出力を保証します。

npmのダウンロードトレンド

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
flatted80,013,5831,12738.5 kB12日前ISC
json-stringify-safe28,369,163554-711年前ISC
circular-json1,429,501610-07年前MIT
json-stringify-deterministic184,7384111.6 kB02年前MIT

JavaScript における循環参照と JSON 文字列化の戦略的選択

JavaScript でオブジェクトを文字列化する際、標準の JSON.stringify は循環参照(Circular Reference)に遭遇するとエラーを投げます。また、オブジェクトのキー順序が保証されないため、同じ内容でも異なる文字列が生成される問題もあります。circular-jsonflattedjson-stringify-deterministicjson-stringify-safe は、これら異なる課題を解決するために登場したパッケージですが、それぞれ役割とトレードオフが明確に異なります。

🚨 最重要:非推奨パッケージの扱い

circular-json は、かつて循環参照を解決する標準的なソリューションでしたが、現在は**非推奨(Deprecated)**です。

  • 作者自身によって flatted への移行が強く推奨されています。
  • バグ修正やセキュリティアップデートは行われていません。
  • 新しいプロジェクトで採用するのはリスクが高く、避けるべきです。
// circular-json: 非推奨 — 使用しないでください
const CircularJSON = require('circular-json');
const obj = { a: 1 };
obj.self = obj;
// 古い API。現在は flatted を使うべき
const json = CircularJSON.stringify(obj); 

代わりに、同じ作者によって作成され、より高速でモダンな flatted を使用します。

// flatted: 推奨 — 現在の標準
import { stringify, parse } from 'flatted';

const obj = { a: 1 };
obj.self = obj;

const json = stringify(obj);
const restored = parse(json); // 循環参照を正しく復元

🔄 循環参照の処理:構造維持 vs 安全な表示

循環参照をどう扱うかは、ユースケースによって大きく 2 つに分かれます。「後で元に戻したい」か「とりあえずクラッシュさせずにログに出したい」かです。

flatted は、循環参照を特別なトークンに置き換えてエンコードし、parse で元の参照関係を復元できます。データ永続化や通信に適しています。

// flatted: 構造を維持して復元可能
import { stringify, parse } from 'flatted';

const user = { name: 'Alice' };
user.friend = user; // 循環参照

const data = stringify(user);
// 出力例: "[{"name":"Alice","friend":"#0"},"#0"]`
const original = parse(data); // user.friend === user が真になる

json-stringify-safe は、復元を諦める代わりに、循環参照を文字列 [Circular] に置き換えてエラーを回避します。ログ出力やエラーレポートに適しています。

// json-stringify-safe: 安全に文字列化(復元不可)
const stringify = require('json-stringify-safe');

const user = { name: 'Alice' };
user.friend = user;

const data = stringify(user, null, 2);
// 出力例:{ "name": "Alice", "friend": "[Circular]" }
// parse しても friend は文字列のまま

circular-jsonflatted と同様に復元を目指していましたが、前述の通り非推奨です。

// circular-json: 非推奨(参考用)
const CircularJSON = require('circular-json');
const data = CircularJSON.stringify({ a: 1 }); // 過去の方法

🔢 出力の決定性:キー順序の固定

標準の JSON.stringify は、オブジェクトのキー順序を保証しません。これにより、同じ内容のオブジェクトでもハッシュ値が異なったり、キャッシュミスが起きたりします。json-stringify-deterministic はこの問題を解決します。

json-stringify-deterministic は、キーをアルファベット順などにソートしてから文字列化します。循環参照の処理は主目的ではありません(組み合わせが必要な場合があります)。

// json-stringify-deterministic: キー順序を固定
const stringify = require('json-stringify-deterministic');

const obj1 = { b: 2, a: 1 };
const obj2 = { a: 1, b: 2 };

const str1 = stringify(obj1); // '{"a":1,"b":2}'
const str2 = stringify(obj2); // '{"a":1,"b":2}'

console.log(str1 === str2); // true — ハッシュ比較などに有用

他のパッケージは、基本的にキー順序を固定しません(flattedjson-stringify-safe は入力された順序に従うことが多いですが、保証は環境依存です)。

// flatted: キー順序固定は主目的ではない
import { stringify } from 'flatted';
// キー順序は入力に依存し、ソートはされない

🛡️ エラーハンドリングと安全性

予期せぬデータが混入した際、アプリケーションをクラッシュさせないことは重要です。

json-stringify-safe は、名前の通り「安全」を最優先します。循環参照だけでなく、toJSON メソッド内のエラーなどもキャッチして、null やプレースホルダーに置き換えるオプションを提供します。

// json-stringify-safe: エラーを握りつぶして継続
const stringify = require('json-stringify-safe');

const obj = {
  value: 1,
  broken: {
    toJSON() { throw new Error('Boom'); }
  }
};

// 標準の JSON.stringify ならここでクラッシュする
const safe = stringify(obj, null, 2, () => '[Error]'); 
// broken プロパティが '[Error]' に置き換わる

flatted は構造の完全性を重視するため、予期せぬエラーに対しては厳格に動作する傾向があります。データの整合性が求められる场合に適しています。

// flatted: データ整合性を重視
import { stringify } from 'flatted';
// エラーが発生すれば例外が投げられ、処理が止まる(想定通り)

🌐 実世界でのシナリオ別選択ガイド

シナリオ 1:ユーザーセッションの保存

Redis などにユーザーオブジェクトを保存し、後で復元する必要があります。循環参照が含まれる可能性があります。

  • 最佳選択: flatted
  • 理由: 復元可能性が必須であり、circular-json は非推奨のため。
import { stringify } from 'flatted';
redis.set('session', stringify(userObject));

シナリオ 2:エラーログの記録

例外オブジェクトには循環参照が含まれることが多く、ログ出力時にクラッシュするのを防ぎたいです。

  • 最佳選択: json-stringify-safe
  • 理由: 復元不要。とにかくエラーを出さずに文字列化できれば良い。
const stringify = require('json-stringify-safe');
logger.error(stringify(errorObject));

シナリオ 3:コンテンツハッシュの生成

API レスポンスのキャッシュキーを生成します。キー順序が違っても同じハッシュ値になる必要があります。

  • 最佳選択: json-stringify-deterministic
  • 理由: 出力の決定性が必須。
const stringify = require('json-stringify-deterministic');
const hash = createHash('sha256').update(stringify(data)).digest('hex');

シナリオ 4:レガシーコードの保守

古いコードベースで circular-json が使われています。

  • 最佳選択: flatted への移行
  • 理由: circular-json はセキュリティリスクとバグの可能性があります。
// 移行前
// const CircularJSON = require('circular-json');
// 移行後
import { stringify } from 'flatted';

📊 機能比較サマリー

機能flattedjson-stringify-safejson-stringify-deterministiccircular-json
循環参照処理✅ エンコードして復元可能[Circular] に置換(復元不可)❌ 基本対応外(throws)✅ エンコードして復元可能
キー順序固定✅ ソートして固定
エラー耐性⚠️ 標準的✅ 高い(toJSON エラー等も捕捉)⚠️ 標準的⚠️ 標準的
メンテナンス✅ 活発✅ 維持されている✅ 維持されている非推奨
主な用途データ保存・通信ログ出力・デバッグハッシュ・キャッシュ(使用すべきでない)

💡 結論:現代のアーキテクチャにおける選択

これら 4 つのパッケージは、同じ「JSON 文字列化」という言葉を使っていても、解決しようとしている問題が異なります。

flatted は、circular-json の正当な後継者として、循環参照を扱う現代の標準です。データ構造を維持して保存・通信する必要がある場合は、これ一択です。

json-stringify-safe は、開発者の代わりにエラーを吸収する「防具」です。ログシステムや監視ツールなど、データの完全性よりプロセスの継続性を優先する場面で輝きます。

json-stringify-deterministic は、出力の「一貫性」を保証するツールです。キャッシュ戦略やデータ整合性チェックなど、同じ入力なら必ず同じ出力が欲しい場合に不可欠です。

circular-json は、過去の遺産です。新しいコードに書く理由はありません。

最終的には、「復元が必要か(flatted)」「安全なログが必要か(json-stringify-safe)」、**「順序の固定が必要か(json-stringify-deterministic)」**という 3 つの軸で要件を整理し、適切なツールを選ぶことが重要です。場合によっては、flatted でエンコードした後に json-stringify-deterministic でソートするといった組み合わせも有効ですが、まずは各パッケージの本来の目的を理解して使用することが、堅牢なフロントエンドアーキテクチャへの第一歩です。

選び方: flatted vs json-stringify-safe vs circular-json vs json-stringify-deterministic

  • flatted:

    循環参照を含むオブジェクトを、後で元の構造に戻せる形式(デシリアライズ可能)で保存したい場合に選択します。ログ保存、キャッシュ、または IPC 通信などでオブジェクトの完全な構造を維持する必要がある現代のアプリケーションに最適です。

  • json-stringify-safe:

    循環参照が含まれている可能性があるオブジェクトを、クラッシュさせずにログ出力したい場合に選択します。元の構造を完全に復元する必要はなく、エラーを出さずに「[Circular]」などのプレースホルダーに置き換えて表示できれば良いという監視やデバッグのシナリオに適しています。

  • circular-json:

    このパッケージは公式に非推奨(Deprecated)となっており、新しいプロジェクトで使用すべきではありません。メンテナンスが停止しており、既知の問題も修正されません。既存のレガシーコードで使われている場合は、後継である flatted への移行を計画してください。

  • json-stringify-deterministic:

    オブジェクトのキー順序が毎回変動してしまうことで問題が起きる場合に選択します。コンテンツのハッシュ値生成、キャッシュキーの作成、またはテストで出力の一致を確認する際など、入力と同じなら出力も必ず同じになる必要がある場景で不可欠です。

flatted のREADME

flatted

Downloads Coverage Status License: ISC WebReflection status

snow flake

Social Media Photo by Matt Seymour on Unsplash

A super light (0.5K) and fast circular JSON parser, directly from the creator of CircularJSON.

Available also for PHP.

Available also for Python.

Available also for Go.


ℹ️ JSON only values

If you need anything more complex than values JSON understands, there is a standard approach to recursion and more data-types than what JSON allows, and it's part of the Structured Clone polyfill.


npm i flatted

Usable via CDN or as regular module.

// ESM
import {parse, stringify, toJSON, fromJSON} from 'flatted';

// CJS
const {parse, stringify, toJSON, fromJSON} = require('flatted');

const a = [{}];
a[0].a = a;
a.push(a);

stringify(a); // [["1","0"],{"a":"0"}]

toJSON and fromJSON

If you'd like to implicitly survive JSON serialization, these two helpers helps:

import {toJSON, fromJSON} from 'flatted';

class RecursiveMap extends Map {
  static fromJSON(any) {
    return new this(fromJSON(any));
  }
  toJSON() {
    return toJSON([...this.entries()]);
  }
}

const recursive = new RecursiveMap;
const same = {};
same.same = same;
recursive.set('same', same);

const asString = JSON.stringify(recursive);
const asMap = RecursiveMap.fromJSON(JSON.parse(asString));
asMap.get('same') === asMap.get('same').same;
// true

Flatted VS JSON

As it is for every other specialized format capable of serializing and deserializing circular data, you should never JSON.parse(Flatted.stringify(data)), and you should never Flatted.parse(JSON.stringify(data)).

The only way this could work is to Flatted.parse(Flatted.stringify(data)), as it is also for CircularJSON or any other, otherwise there's no granted data integrity.

Also please note this project serializes and deserializes only data compatible with JSON, so that sockets, or anything else with internal classes different from those allowed by JSON standard, won't be serialized and unserialized as expected.

New in V1: Exact same JSON API

  • Added a reviver parameter to .parse(string, reviver) and revive your own objects.
  • Added a replacer and a space parameter to .stringify(object, replacer, space) for feature parity with JSON signature.

Compatibility

All ECMAScript engines compatible with Map, Set, Object.keys, and Array.prototype.reduce will work, even if polyfilled.

How does it work ?

While stringifying, all Objects, including Arrays, and strings, are flattened out and replaced as unique index. *

Once parsed, all indexes will be replaced through the flattened collection.

* represented as string to avoid conflicts with numbers

// logic example
var a = [{one: 1}, {two: '2'}];
a[0].a = a;
// a is the main object, will be at index '0'
// {one: 1} is the second object, index '1'
// {two: '2'} the third, in '2', and it has a string
// which will be found at index '3'

Flatted.stringify(a);
// [["1","2"],{"one":1,"a":"0"},{"two":"3"},"2"]
// a[one,two]    {one: 1, a}    {two: '2'}  '2'