Standard JSON.stringify() fails when encountering circular references and does not guarantee consistent key ordering across environments. These four packages offer specialized solutions: circular-json and flatted enable round-trip serialization of circular structures, json-stringify-safe prevents crashes during logging by masking circles, and json-stringify-deterministic ensures consistent key ordering for hashing or signing.
Standard JSON.stringify() is powerful but has two major limitations for advanced engineering: it throws an error on circular references, and it does not guarantee consistent key ordering. The packages circular-json, flatted, json-stringify-deterministic, and json-stringify-safe address these gaps in different ways. Let's compare how they handle real-world serialization challenges.
The most common failure point in JavaScript serialization is the circular reference β when object A points to object B, which points back to object A.
circular-json was an early solution that allowed serialization and parsing of circular structures.
// circular-json: Deprecated API
import CircularJSON from 'circular-json';
const obj = { name: 'test' };
obj.self = obj;
// Works, but package is unmaintained
const str = CircularJSON.stringify(obj);
const parsed = CircularJSON.parse(str);
flatted is the modern replacement for circular-json.
// flatted: Modern API
import { stringify, parse } from 'flatted';
const obj = { name: 'test' };
obj.self = obj;
// Recommended approach
const str = stringify(obj);
const parsed = parse(str); // parsed.self === parsed
json-stringify-safe handles circles by masking them, not preserving them.
// json-stringify-safe: Logging API
import stringify from 'json-stringify-safe';
const obj = { name: 'test' };
obj.self = obj;
// Does not throw, but output is for reading only
const str = stringify(obj);
// Output: {"name":"test","self":"[Circular]"}
json-stringify-deterministic focuses on key order, not circles.
// json-stringify-deterministic: Key Order API
import stringify from 'json-stringify-deterministic';
const obj = { name: 'test' };
obj.self = obj;
// Will throw TypeError: Converting circular structure to JSON
const str = stringify(obj);
When generating hashes or cache keys, "{a:1, b:2}" must equal "{b:2, a:1}". Standard JSON does not guarantee this.
circular-json does not guarantee key order.
// circular-json: No order guarantee
import CircularJSON from 'circular-json';
const obj = { b: 2, a: 1 };
const str = CircularJSON.stringify(obj);
// Key order depends on insertion, not sorted
flatted does not guarantee key order.
// flatted: No order guarantee
import { stringify } from 'flatted';
const obj = { b: 2, a: 1 };
const str = stringify(obj);
// Key order depends on insertion, not sorted
json-stringify-deterministic sorts keys alphabetically.
// json-stringify-deterministic: Sorted Keys
import stringify from 'json-stringify-deterministic';
const obj = { b: 2, a: 1 };
const str = stringify(obj);
// Output: {"a":1,"b":2} (Always sorted)
json-stringify-safe does not guarantee key order.
// json-stringify-safe: No order guarantee
import stringify from 'json-stringify-safe';
const obj = { b: 2, a: 1 };
const str = stringify(obj);
// Key order depends on insertion, not sorted
How these packages fit into your existing codebase varies by design.
circular-json uses a default export that mimics JSON.
// circular-json: Legacy namespace
import CircularJSON from 'circular-json';
CircularJSON.stringify(data);
flatted uses named exports for clarity.
stringify and parse functions.// flatted: Named exports
import { stringify, parse } from 'flatted';
stringify(data);
json-stringify-deterministic exports a single function.
stringify.// json-stringify-deterministic: Single function
import stringify from 'json-stringify-deterministic';
stringify(data);
json-stringify-safe exports a single function with optional arguments.
JSON.stringify signature (obj, replacer, space).// json-stringify-safe: Standard signature
import stringify from 'json-stringify-safe';
stringify(data, null, 2);
You have a complex state tree with circular references (e.g., parent-child links).
flatted// flatted: State persistence
import { stringify, parse } from 'flatted';
localStorage.setItem('state', stringify(appState));
const restored = parse(localStorage.getItem('state'));
You are logging request objects that might contain circular references (e.g., req.res.req).
json-stringify-safe[Circular].// json-stringify-safe: Error logging
import stringify from 'json-stringify-safe';
logger.error(stringify(errorObject));
You need to hash a payload to verify integrity. Key order must be consistent.
json-stringify-deterministichash({a:1}) must equal hash({a:1}) regardless of how the object was created.// json-stringify-deterministic: Signing
import stringify from 'json-stringify-deterministic';
const signature = crypto.createHash('sha256').update(stringify(payload)).digest('hex');
You encounter circular-json in an old codebase.
flattedcircular-json is deprecated and may have unpatched vulnerabilities.// Refactor target
// Old: import CircularJSON from 'circular-json';
// New: import { stringify } from 'flatted';
| Feature | circular-json | flatted | json-stringify-safe | json-stringify-deterministic |
|---|---|---|---|---|
| Circular Refs | β Supported | β Supported | β οΈ Masked ([Circular]) | β Throws Error |
| Parseable | β Yes | β Yes | β No | β Yes |
| Key Order | β Unsorted | β Unsorted | β Unsorted | β Sorted |
| Status | β Deprecated | β Active | β Stable | β Stable |
| Best For | Legacy Support | State Sync | Logging | Hashing/Signing |
flatted is the go-to solution for data that needs to travel and return with its structure intact. It solves the circular reference problem without sacrificing the ability to parse the data later.
json-stringify-safe is a safety net for observability. It ensures your logging infrastructure never crashes due to unexpected object shapes, trading data fidelity for stability.
json-stringify-deterministic is a tool for integrity. It removes ambiguity from serialization, making it essential for security and caching layers.
circular-json belongs in the past. It served a purpose but has been superseded by better, maintained alternatives.
Final Thought: Don't reach for a circular serializer when you just need to log an error, and don't use a safe logger when you need to generate a hash. Match the tool to the data flow requirement.
Do not use this package in new projects. It is officially deprecated and unmaintained. The author explicitly recommends switching to flatted, which offers better performance and compatibility. Using this introduces technical debt and potential security risks.
Choose flatted when you need to save and reload complex state objects that contain circular references. It is the modern standard for round-trip serialization, allowing you to stringify and parse data back into its original structure without losing references.
Choose json-stringify-deterministic when you need to generate checksums, signatures, or cache keys from objects. It ensures that two objects with the same data but different key insertion orders produce the exact same JSON string.
Choose json-stringify-safe for logging and error reporting where you want to avoid crashes if an object happens to be circular. It replaces circular references with a string marker like [Circular] instead of throwing an error, but the result cannot be parsed back.
Serializes and deserializes otherwise valid JSON objects containing circular references into and from a specialized JSON format.
Smaller, faster, and able to produce on average a reduced output too, flatted is the new, bloatless, ESM and CJS compatible, circular JSON parser.
It has now reached V1 and it implements the exact same JSON API.
Please note CircularJSON is in maintenance only and flatted is its successor.
A usage example:
var object = {};
object.arr = [
object, object
];
object.arr.push(object.arr);
object.obj = object;
var serialized = CircularJSON.stringify(object);
// '{"arr":["~","~","~arr"],"obj":"~"}'
// NOTE: CircularJSON DOES NOT parse JS
// it handles receiver and reviver callbacks
var unserialized = CircularJSON.parse(serialized);
// { arr: [ [Circular], [Circular] ],
// obj: [Circular] }
unserialized.obj === unserialized;
unserialized.arr[0] === unserialized;
unserialized.arr.pop() === unserialized.arr;
A quick summary:
0.5, you can specify a JSON parser different from JSON itself. CircularJSON.parser = ABetterJSON; is all you need.~ as a special prefix symbol to denote which parent the reference belongs to (i.e. ~root~child1~child2)npm install --save circular-json
'use strict';
var
CircularJSON = require('circular-json'),
obj = { foo: 'bar' },
str
;
obj.self = obj;
str = CircularJSON.stringify(obj);
There are no dependencies.
(generated via gitstrap)
<script src="build/circular-json.js"></script>
'use strict';
var CircularJSON = window.CircularJSON
, obj = { foo: 'bar' }
, str
;
obj.self = obj;
str = CircularJSON.stringify(obj);
NOTE: Platforms without native JSON (i.e. MSIE <= 8) requires json3.js or similar.
It is also a bad idea to CircularJSON.parse(JSON.stringify(object)) because of those manipulation used in CircularJSON.stringify() able to make parsing safe and secure.
As summary: CircularJSON.parse(CircularJSON.stringify(object)) is the way to go, same is for JSON.parse(JSON.stringify(object)).
It's the same as native JSON, except the fourth parameter placeholder, which circular references to be replaced with "[Circular]" (i.e. for logging).
Bear in mind JSON.parse(CircularJSON.stringify(object)) will work but not produce the expected output.
The module json-stringify-safe seems to be for console.log() but it's completely pointless for JSON.parse(), being latter one unable to retrieve back the initial structure. Here an example:
// a logged object with circular references
{
"circularRef": "[Circular]",
"list": [
"[Circular]",
"[Circular]"
]
}
// what do we do with above output ?
Just type this in your node console: var o = {}; o.a = o; console.log(o);. The output will be { a: [Circular] } ... good, but that ain't really solving the problem.
However, if that's all you need, the function used to create that kind of output is probably faster than CircularJSON and surely fits in less lines of code.
So here the thing: circular references can be wrong but, if there is a need for them, any attempt to ignore them or remove them can be considered just a failure.
Not because the method is bad or it's not working, simply because the circular info, the one we needed and used in the first place, is lost!
In this case, CircularJSON does even more than just solve circular and recursions: it maps all same objects so that less memory is used as well on deserialization as less bandwidth too!
It's able to redefine those references back later on so the way we store is the way we retrieve and in a reasonably performant way, also trusting the snappy and native JSON methods to iterate.