circular-json、flatted、json-stringify-deterministic、json-stringify-safe は、すべて JavaScript の JSON.stringify が抱える課題を解決するためのユーティリティですが、それぞれ異なる問題に焦点を当てています。circular-json と flatted は循環参照(Circular Reference)を含むオブジェクトを完全にシリアライズ・デシリアライズすることを目的としていますが、circular-json は現在非推奨であり flatted への移行が推奨されています。一方、json-stringify-safe は循環参照 encountered 時にクラッシュせず安全に文字列化することを優先し、json-stringify-deterministic はオブジェクトのキー順序を固定してハッシュ生成やキャッシュ比較に適した出力を保証します。
JavaScript でオブジェクトを文字列化する際、標準の JSON.stringify は循環参照(Circular Reference)に遭遇するとエラーを投げます。また、オブジェクトのキー順序が保証されないため、同じ内容でも異なる文字列が生成される問題もあります。circular-json、flatted、json-stringify-deterministic、json-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); // 循環参照を正しく復元
循環参照をどう扱うかは、ユースケースによって大きく 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-json は flatted と同様に復元を目指していましたが、前述の通り非推奨です。
// 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 — ハッシュ比較などに有用
他のパッケージは、基本的にキー順序を固定しません(flatted や json-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';
// エラーが発生すれば例外が投げられ、処理が止まる(想定通り)
Redis などにユーザーオブジェクトを保存し、後で復元する必要があります。循環参照が含まれる可能性があります。
flattedcircular-json は非推奨のため。import { stringify } from 'flatted';
redis.set('session', stringify(userObject));
例外オブジェクトには循環参照が含まれることが多く、ログ出力時にクラッシュするのを防ぎたいです。
json-stringify-safeconst stringify = require('json-stringify-safe');
logger.error(stringify(errorObject));
API レスポンスのキャッシュキーを生成します。キー順序が違っても同じハッシュ値になる必要があります。
json-stringify-deterministicconst stringify = require('json-stringify-deterministic');
const hash = createHash('sha256').update(stringify(data)).digest('hex');
古いコードベースで circular-json が使われています。
flatted への移行circular-json はセキュリティリスクとバグの可能性があります。// 移行前
// const CircularJSON = require('circular-json');
// 移行後
import { stringify } from 'flatted';
| 機能 | flatted | json-stringify-safe | json-stringify-deterministic | circular-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 でソートするといった組み合わせも有効ですが、まずは各パッケージの本来の目的を理解して使用することが、堅牢なフロントエンドアーキテクチャへの第一歩です。
循環参照を含むオブジェクトを、後で元の構造に戻せる形式(デシリアライズ可能)で保存したい場合に選択します。ログ保存、キャッシュ、または IPC 通信などでオブジェクトの完全な構造を維持する必要がある現代のアプリケーションに最適です。
循環参照が含まれている可能性があるオブジェクトを、クラッシュさせずにログ出力したい場合に選択します。元の構造を完全に復元する必要はなく、エラーを出さずに「[Circular]」などのプレースホルダーに置き換えて表示できれば良いという監視やデバッグのシナリオに適しています。
このパッケージは公式に非推奨(Deprecated)となっており、新しいプロジェクトで使用すべきではありません。メンテナンスが停止しており、既知の問題も修正されません。既存のレガシーコードで使われている場合は、後継である flatted への移行を計画してください。
オブジェクトのキー順序が毎回変動してしまうことで問題が起きる場合に選択します。コンテンツのハッシュ値生成、キャッシュキーの作成、またはテストで出力の一致を確認する際など、入力と同じなら出力も必ず同じになる必要がある場景で不可欠です。

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.
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"}]
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
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.
.parse(string, reviver) and revive your own objects.space parameter to .stringify(object, replacer, space) for feature parity with JSON signature.All ECMAScript engines compatible with Map, Set, Object.keys, and Array.prototype.reduce will work, even if polyfilled.
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'