These six npm packages solve different problems around JSON stringification in JavaScript applications. While native JSON.stringify() works for simple cases, it fails with circular references, produces non-deterministic key ordering, and lacks performance optimizations for large datasets. circular-json and flatted handle circular references by encoding them specially. fast-json-stringify achieves extreme speed through schema validation. fast-stable-stringify and json-stable-stringify ensure consistent key ordering for hashing or comparison. json-stringify-safe prevents crashes on circular structures by replacing them with placeholder strings. Choosing the right tool depends on whether you prioritize speed, safety, determinism, or circular reference support.
Native JSON.stringify() works well for simple data, but real-world applications often hit its limits. Circular references crash it. Key ordering changes between runs. Performance drops on large objects. The six packages in this comparison each solve specific problems that native JSON cannot handle. Let's examine how they differ and when to use each one.
Before diving into features, note that circular-json is deprecated. The maintainer officially recommends migrating to flatted. Using deprecated packages in new projects introduces security and maintenance risks.
// circular-json - DEPRECATED, do not use in new projects
import CircularJSON from 'circular-json';
const str = CircularJSON.stringify(obj); // Works but unmaintained
// flatted - Recommended replacement
import { stringify } from 'flatted';
const str = stringify(obj); // Same capability, actively maintained
json-stringify-safe is also in maintenance mode with minimal updates. It still works but lacks modern optimizations. Consider flatted if you need circular reference support with active maintenance.
Circular references occur when objects reference themselves or each other. Native JSON.stringify() throws TypeError: Converting circular structure to JSON.
flatted encodes circular references so they can be revived later.
import { stringify, parse } from 'flatted';
const obj = { name: 'parent' };
obj.self = obj; // Circular reference
const str = stringify(obj); // '{"name":"parent","self":"~0"}'
const revived = parse(str); // Circular reference restored
console.log(revived.self === revived); // true
circular-json works similarly but is deprecated.
import CircularJSON from 'circular-json';
const obj = { name: 'parent' };
obj.self = obj;
const str = CircularJSON.stringify(obj);
const revived = CircularJSON.parse(str);
console.log(revived.self === revived); // true
json-stringify-safe replaces circular references with placeholder strings instead of encoding them.
import stringify from 'json-stringify-safe';
const obj = { name: 'parent' };
obj.self = obj;
const str = stringify(obj);
// '{"name":"parent","self":"[Circular ~]"}'
// Cannot be parsed back to restore circular reference
fast-json-stringify, fast-stable-stringify, and json-stable-stringify do not handle circular references. They will throw errors if circular structures are passed.
import fastJsonStringify from 'fast-json-stringify';
const stringify = fastJsonStringify({
type: 'object',
properties: { name: { type: 'string' } }
});
const obj = { name: 'test' };
obj.self = obj; // Will throw error
stringify(obj); // TypeError: Converting circular structure to JSON
fast-json-stringify is the fastest option but requires a JSON schema.
import fastJsonStringify from 'fast-json-stringify';
const stringify = fastJsonStringify({
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
active: { type: 'boolean' }
},
required: ['id', 'name']
});
const obj = { id: 1, name: 'User', active: true };
const str = stringify(obj); // Extremely fast, schema-validated
fast-stable-stringify offers good performance without schema requirements.
import fastStableStringify from 'fast-stable-stringify';
const obj = { z: 1, a: 2, m: 3 };
const str = fastStableStringify(obj); // '{"a":2,"m":3,"z":1}'
// Keys sorted alphabetically, faster than json-stable-stringify
json-stable-stringify prioritizes compatibility over speed.
import stableStringify from 'json-stable-stringify';
const obj = { z: 1, a: 2, m: 3 };
const str = stableStringify(obj); // '{"a":2,"m":3,"z":1}'
// Keys sorted alphabetically, well-tested but slower
flatted and json-stringify-safe have overhead from circular reference handling.
import { stringify as flattedStringify } from 'flatted';
import safeStringify from 'json-stringify-safe';
const obj = { a: 1, b: 2 };
const flattedStr = flattedStringify(obj); // Extra encoding overhead
const safeStr = safeStringify(obj); // Circular check overhead
Native JSON.stringify() does not guarantee key order across JavaScript engines. This breaks hashing, caching, and comparison logic.
fast-stable-stringify sorts keys alphabetically with high performance.
import fastStableStringify from 'fast-stable-stringify';
const obj1 = { id: 1, name: 'Test' };
const obj2 = { name: 'Test', id: 1 };
const str1 = fastStableStringify(obj1); // '{"id":1,"name":"Test"}'
const str2 = fastStableStringify(obj2); // '{"id":1,"name":"Test"}'
console.log(str1 === str2); // true - consistent for hashing
json-stable-stringify also sorts keys with custom comparator support.
import stableStringify from 'json-stable-stringify';
const obj = { b: 2, a: 1 };
// Default alphabetical sort
const str1 = stableStringify(obj); // '{"a":1,"b":2}'
// Custom comparator (e.g., sort by value)
const str2 = stableStringify(obj, {
cmp: (a, b) => a.value < b.value ? -1 : 1
});
flatted preserves insertion order but encodes circular references.
import { stringify } from 'flatted';
const obj = { z: 1, a: 2 };
const str = stringify(obj); // Order preserved but with encoding layer
fast-json-stringify, circular-json, and json-stringify-safe do not guarantee deterministic ordering without additional configuration.
json-stringify-safe never throws β it gracefully degrades on circular references.
import safeStringify from 'json-stringify-safe';
const obj = { name: 'test' };
obj.ref = obj;
const str = safeStringify(obj, null, 2, function(key, value) {
if (value === '[Circular]') {
return undefined; // Remove circular refs entirely
}
return value;
});
// Safe for logging β never crashes
flatted handles circular references but can throw on non-serializable values.
import { stringify } from 'flatted';
const obj = { fn: () => {} }; // Functions not serializable
try {
const str = stringify(obj); // May throw or skip functions
} catch (e) {
// Handle serialization error
}
fast-json-stringify validates against schema and throws on mismatch.
import fastJsonStringify from 'fast-json-stringify';
const stringify = fastJsonStringify({
type: 'object',
properties: { id: { type: 'number' } }
});
stringify({ id: 'not-a-number' }); // Throws validation error
// This is a feature β catches data issues early
fast-stable-stringify and json-stable-stringify behave like native JSON.stringify() for errors.
import fastStableStringify from 'fast-stable-stringify';
const obj = { value: BigInt(1) }; // BigInt not JSON-serializable
fastStableStringify(obj); // TypeError: Do not know how to serialize a BigInt
High-throughput REST API with known response shapes.
fast-json-stringifyconst stringify = fastJsonStringify({
type: 'object',
properties: {
status: { type: 'string' },
data: { type: 'array', items: { type: 'object' } }
}
});
app.get('/users', (req, res) => {
const response = { status: 'ok', data: users };
res.send(stringify(response)); // Fast, validated
});
Saving application state that contains self-referential objects.
flattedimport { stringify, parse } from 'flatted';
// Save state
const state = createAppState(); // May contain circular refs
localStorage.setItem('app-state', stringify(state));
// Load state
const loaded = parse(localStorage.getItem('app-state'));
// Circular references restored
Creating consistent hash keys from configuration objects.
fast-stable-stringifyimport fastStableStringify from 'fast-stable-stringify';
import { createHash } from 'crypto';
function createCacheKey(config) {
const normalized = fastStableStringify(config);
return createHash('sha256').update(normalized).digest('hex');
}
// Same config, different key order = same hash
Serializing error objects that may contain circular references.
json-stringify-safeimport safeStringify from 'json-stringify-safe';
logger.error({
message: 'Request failed',
error: err, // May have circular refs
context: req // May reference error
});
// Internally uses safeStringify to prevent crashes
Comparing objects in unit tests where order should not matter.
json-stable-stringifyimport stableStringify from 'json-stable-stringify';
expect(stableStringify(actual)).toBe(stableStringify(expected));
// Order-independent comparison
| Package | CircularRefs | DeterministicOrder | SchemaRequired | Performance | Maintenance |
|---|---|---|---|---|---|
circular-json | β Encode/Decode | β | β | Medium | β Deprecated |
fast-json-stringify | β Throws | β | β Required | β‘ Very High | β Active |
fast-stable-stringify | β Throws | β Sorted | β | β‘ High | β Active |
flatted | β Encode/Decode | β | β | Medium | β Active |
json-stable-stringify | β Throws | β Sorted | β | Medium | β οΈ Maintenance |
json-stringify-safe | β Placeholder | β | β | Medium | β οΈ Maintenance |
Despite their differences, these packages share some common ground:
All packages produce valid JSON strings (except flatted and circular-json which use special encoding).
// All produce string output
const str1 = fastStableStringify(obj);
const str2 = stableStringify(obj);
const str3 = safeStringify(obj);
// All are valid JSON (parseable with JSON.parse except flatted/circular-json)
Most support custom replacer functions for transforming values.
// fast-stable-stringify
fastStableStringify(obj, (key, value) => {
if (key.startsWith('_')) return undefined; // Hide private keys
return value;
});
// json-stable-stringify
stableStringify(obj, {
replacer: (key, value) => value
});
// json-stringify-safe
safeStringify(obj, (key, value) => value, 2);
Pretty-printing supported across most packages.
// fast-stable-stringify
fastStableStringify(obj, null, 2); // 2-space indent
// json-stable-stringify
stableStringify(obj, { space: 2 });
// json-stringify-safe
safeStringify(obj, null, 2);
For performance-critical APIs β fast-json-stringify wins if you can define schemas. The speed gain is substantial for high-throughput systems.
For circular references β flatted is the modern choice. Avoid circular-json in new code. Use json-stringify-safe for logging where revival is not needed.
For deterministic output β fast-stable-stringify offers the best balance of speed and consistency. Choose json-stable-stringify for maximum compatibility and custom comparators.
Final Thought: Native JSON.stringify() remains the default for simple cases. These tools solve specific edge cases β pick based on your actual requirements, not hypothetical needs. Adding dependencies has costs, so only reach for these when native JSON falls short.
Avoid circular-json in new projects β it is officially deprecated and unmaintained. The author recommends migrating to flatted instead, which offers the same circular reference handling with better performance and ongoing support. Only consider this if you are maintaining legacy code that cannot be updated.
Choose fast-json-stringify when performance is critical and you can define a JSON schema for your data. It is 5-10x faster than native JSON.stringify() but requires upfront schema definition. Ideal for high-throughput APIs, logging systems, or serialization bottlenecks where data structure is predictable.
Choose fast-stable-stringify when you need deterministic key ordering with better performance than json-stable-stringify. It is useful for generating consistent hashes, caching keys, or comparing objects where property order matters. Best for medium to large objects where speed matters.
Choose flatted when you need to serialize objects with circular references in modern applications. It is the official successor to circular-json with active maintenance. Use it for state persistence, deep cloning, or any scenario where native JSON.stringify() would throw errors on circular structures.
Choose json-stable-stringify when you need deterministic output and compatibility is more important than raw speed. It is well-tested and widely used for generating consistent string representations of objects. Suitable for test assertions, cache keys, or configuration comparison where performance is not critical.
Choose json-stringify-safe when you need defensive serialization that never throws errors. It replaces circular references with placeholder strings instead of encoding them for later revival. Best for logging, error reporting, or debugging scenarios where data loss is acceptable but crashes are not.
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.