fast-safe-stringify vs json-stringify-safe vs safe-json-stringify vs safe-stable-stringify
Handling Circular References and Deterministic Output in JSON Serialization
fast-safe-stringifyjson-stringify-safesafe-json-stringifysafe-stable-stringifySimilar Packages:

Handling Circular References and Deterministic Output in JSON Serialization

Standard JSON.stringify() crashes when encountering circular references, a common issue in complex state management, logging, and error reporting. The packages fast-safe-stringify, json-stringify-safe, safe-json-stringify, and safe-stable-stringify solve this by detecting cycles and replacing them with placeholder strings like "[Circular]". Beyond safety, they differ significantly in performance, memory usage, and whether they guarantee a consistent key order (determinism), which is critical for hashing, caching, and testing.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
fast-safe-stringify0359-95 years agoMIT
json-stringify-safe0556-711 years agoISC
safe-json-stringify057-38 years agoMIT
safe-stable-stringify025230.7 kB72 years agoMIT

Safe JSON Serialization: Performance, Stability, and Circular Reference Handling

In JavaScript, JSON.stringify() is the standard tool for converting objects to strings. However, it has a fatal flaw: it throws a TypeError if the object graph contains a circular reference. This happens frequently in real-world apps—think of DOM nodes, complex error objects with cause chains, or mutually referenced state models.

To fix this, developers turn to "safe" stringifiers. But not all are built the same. Some prioritize raw speed, others guarantee that the output string is always identical (deterministic), and some are simply outdated. Let's break down how fast-safe-stringify, json-stringify-safe, safe-json-stringify, and safe-stable-stringify actually work under the hood.

🛡️ Core Safety: Handling Circular References

All four packages solve the crash problem by detecting cycles and replacing the circular link with a placeholder string (usually "[Circular]"). However, the way they detect these cycles impacts performance.

fast-safe-stringify uses a highly optimized approach that avoids heavy memory overhead. It detects cycles quickly without slowing down the serialization of normal objects.

import stringify from 'fast-safe-stringify';

const obj = { name: 'Alice' };
obj.self = obj; // Circular reference

// Output: '{"name":"Alice","self":"[Circular]"}'
console.log(stringify(obj));

json-stringify-safe was the pioneer in this space. It works reliably but uses an older detection method that can be slower on deeply nested structures.

import stringify from 'json-stringify-safe';

const obj = { name: 'Alice' };
obj.self = obj;

// Output: '{"name":"Alice","self":"[Circular]"}'
console.log(stringify(obj));

safe-json-stringify provides similar safety guarantees. It traverses the object and swaps out circular refs, ensuring your app doesn't crash.

import stringify from 'safe-json-stringify';

const obj = { name: 'Alice' };
obj.self = obj;

// Output: '{"name":"Alice","self":"[Circular]"}'
console.log(stringify(obj));

safe-stable-stringify also handles cycles safely, but it does extra work to ensure the order of keys remains consistent, which we will discuss in the next section.

import stringify from 'safe-stable-stringify';

const obj = { name: 'Alice' };
obj.self = obj;

// Output: '{"name":"Alice","self":"[Circular]"}'
console.log(stringify(obj));

⚡ Performance: Raw Speed vs. Extra Features

When serializing large logs or high-frequency data, speed matters. The trade-off here is usually between "fastest possible" and "deterministic output."

fast-safe-stringify lives up to its name. It is currently the fastest option for most use cases where key order doesn't matter. It mimics the native JSON.stringify behavior closely, preserving the insertion order of keys while adding cycle detection.

// Best for high-volume logging where order doesn't matter
import stringify from 'fast-safe-stringify';

const largeData = generateHugeObject();
const json = stringify(largeData); // Minimal overhead

json-stringify-safe and safe-json-stringify are noticeably slower. They were written years ago when JavaScript engines were less optimized for these patterns. In modern benchmarks, they often lag behind fast-safe-stringify by a significant margin.

// Legacy performance profile
import stringify from 'json-stringify-safe';

const json = stringify(largeData); // Higher CPU usage compared to modern libs

safe-stable-stringify sacrifices some raw speed to guarantee stability. Sorting keys or enforcing a specific traversal order takes extra computation. If you need stability, this cost is worth it; if you just need speed, it might be overkill.

// Pays a small performance tax for deterministic output
import stringify from 'safe-stable-stringify';

const json = stringify(largeData); // Slower than fast-safe-stringify, but stable

🔒 Determinism: Why Key Order Matters

This is the most critical architectural difference. Native JSON.stringify outputs keys in the order they were added to the object. If you create an object with keys {a, b} in one run and {b, a} in another (perhaps due to refactoring or different code paths), the resulting JSON strings will be different, even if the data is the same.

This breaks:

  • Content-based caching (CDN misses when it shouldn't)
  • Object hashing (signatures fail)
  • Snapshot testing (tests fail despite no logic change)

fast-safe-stringify, json-stringify-safe, and safe-json-stringify do NOT guarantee stable key ordering. They preserve insertion order. If your code creates objects differently, your strings will differ.

// UNSTABLE: Output depends on how 'obj' was constructed
import stringify from 'fast-safe-stringify';

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

console.log(stringify(obj1)); // '{"a":1,"b":2}'
console.log(stringify(obj2)); // '{"b":2,"a":1}' -> Different string!

safe-stable-stringify is the only one in this list designed specifically to solve this. It recursively sorts keys or uses a deterministic traversal strategy so that equivalent objects always produce the exact same string, regardless of creation order.

// STABLE: Output is always sorted/consistent
import stringify from 'safe-stable-stringify';

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

console.log(stringify(obj1)); // '{"a":1,"b":2}'
console.log(stringify(obj2)); // '{"a":1,"b":2}' -> Identical string!

🏗️ Architecture & Maintenance Status

Choosing a library isn't just about features; it's about long-term viability.

json-stringify-safe is effectively deprecated for new work. It hasn't seen significant updates in years. While it still works, relying on unmaintained packages for core infrastructure like serialization introduces unnecessary risk.

safe-json-stringify suffers from similar issues. It is a simple utility that hasn't evolved to match modern engine optimizations.

fast-safe-stringify is actively maintained and widely adopted in the ecosystem (used by major logging libraries). It strikes the best balance for general use.

safe-stable-stringify is also actively maintained and has become the standard for tools requiring reproducibility, such as build tools, testing frameworks, and cryptographic utilities.

📊 Real-World Selection Guide

Scenario 1: High-Volume Application Logging

You are logging thousands of request objects per second. Some objects might have circular refs (e.g., from Express request contexts). You need maximum throughput.

  • Choice: fast-safe-stringify
  • Why: You don't care if key order changes between runs; you just need the log to not crash and to be fast.
logger.info(fastSafeStringify(reqObject));

Scenario 2: Generating Cache Keys or Hashes

You are creating a cache key based on a configuration object. If the string representation changes due to key ordering, the cache lookup will fail, causing performance issues.

  • Choice: safe-stable-stringify
  • Why: You must ensure { port: 80, host: 'localhost' } produces the same hash as { host: 'localhost', port: 80 }.
const cacheKey = crypto.createHash('md5')
  .update(safeStableStringify(config))
  .digest('hex');

Scenario 3: Legacy System Maintenance

You are fixing a bug in a 7-year-old Node.js service that already uses json-stringify-safe.

  • Choice: Keep json-stringify-safe (or refactor if possible)
  • Why: Changing serialization logic in legacy systems can introduce subtle bugs if tests rely on specific (non-deterministic) string outputs. Only switch if you have full test coverage.
// Existing legacy code
const payload = jsonStringifySafe(complexErrorObject);

📌 Summary Comparison

Featurefast-safe-stringifyjson-stringify-safesafe-json-stringifysafe-stable-stringify
Circular Safety✅ Yes✅ Yes✅ Yes✅ Yes
Performance🚀 Very Fast🐢 Slow🐢 Slow⚡ Fast (with overhead)
Deterministic Keys❌ No (Insertion Order)❌ No❌ NoYes (Sorted/Stable)
Maintenance Status🟢 Active🔴 Inactive/Legacy🟡 Low Activity🟢 Active
Best Use CaseLogging, APIsLegacy SupportLegacy SupportHashing, Caching, Tests

💡 The Bottom Line

For most modern frontend and Node.js applications, fast-safe-stringify is the default winner. It prevents crashes without slowing down your app, making it perfect for logging and data transport where exact key order doesn't impact logic.

However, if your architecture relies on reproducibility—such as generating content hashes for caching, signing data, or running snapshot tests—you must use safe-stable-stringify. The slight performance cost is a necessary trade-off to ensure your system behaves predictably.

Avoid json-stringify-safe and safe-json-stringify for new projects. They represent an older generation of solutions that have been surpassed by faster, more robust alternatives. Stick to the actively maintained tools to keep your dependency tree healthy.

How to Choose: fast-safe-stringify vs json-stringify-safe vs safe-json-stringify vs safe-stable-stringify

  • fast-safe-stringify:

    Choose fast-safe-stringify for general-purpose applications where performance is the top priority and key order does not matter. It is the fastest option for large datasets and is actively maintained, making it the default choice for high-throughput logging or API responses where deterministic output is not required.

  • json-stringify-safe:

    Avoid json-stringify-safe for new projects. While it was the original solution for circular references, it is no longer actively maintained and is significantly slower than modern alternatives. Only use it if you are maintaining a legacy codebase that already depends on it and cannot tolerate a refactor.

  • safe-json-stringify:

    Select safe-json-stringify only if you are working in a very specific legacy environment or need a extremely minimal footprint without modern optimizations. Like json-stringify-safe, it lacks the performance benefits and deterministic features of newer packages and is generally not recommended for modern architecture.

  • safe-stable-stringify:

    Pick safe-stable-stringify when you need guaranteed deterministic output (stable key ordering) alongside circular reference safety. This is essential for generating consistent hashes, comparing objects in tests, or caching mechanisms where the string representation must remain identical across different runs, even if object key insertion order varies.

README for fast-safe-stringify

fast-safe-stringify

Safe and fast serialization alternative to JSON.stringify.

Gracefully handles circular structures instead of throwing in most cases. It could return an error string if the circular object is too complex to analyze, e.g. in case there are proxies involved.

Provides a deterministic ("stable") version as well that will also gracefully handle circular structures. See the example below for further information.

Usage

The same as JSON.stringify.

stringify(value[, replacer[, space[, options]]])

const safeStringify = require('fast-safe-stringify')
const o = { a: 1 }
o.o = o

console.log(safeStringify(o))
// '{"a":1,"o":"[Circular]"}'
console.log(JSON.stringify(o))
// TypeError: Converting circular structure to JSON

function replacer(key, value) {
  console.log('Key:', JSON.stringify(key), 'Value:', JSON.stringify(value))
  // Remove the circular structure
  if (value === '[Circular]') {
    return
  }
  return value
}

// those are also defaults limits when no options object is passed into safeStringify
// configure it to lower the limit.
const options = {
  depthLimit: Number.MAX_SAFE_INTEGER,
  edgesLimit: Number.MAX_SAFE_INTEGER
};

const serialized = safeStringify(o, replacer, 2, options)
// Key: "" Value: {"a":1,"o":"[Circular]"}
// Key: "a" Value: 1
// Key: "o" Value: "[Circular]"
console.log(serialized)
// {
//  "a": 1
// }

Using the deterministic version also works the same:

const safeStringify = require('fast-safe-stringify')
const o = { b: 1, a: 0 }
o.o = o

console.log(safeStringify(o))
// '{"b":1,"a":0,"o":"[Circular]"}'
console.log(safeStringify.stableStringify(o))
// '{"a":0,"b":1,"o":"[Circular]"}'
console.log(JSON.stringify(o))
// TypeError: Converting circular structure to JSON

A faster and side-effect free implementation is available in the [safe-stable-stringify][] module. However it is still considered experimental due to a new and more complex implementation.

Replace strings constants

  • [Circular] - when same reference is found
  • [...] - when some limit from options object is reached

Differences to JSON.stringify

In general the behavior is identical to JSON.stringify. The replacer and space options are also available.

A few exceptions exist to JSON.stringify while using toJSON or replacer:

Regular safe stringify

  • Manipulating a circular structure of the passed in value in a toJSON or the replacer is not possible! It is possible for any other value and property.

  • In case a circular structure is detected and the replacer is used it will receive the string [Circular] as the argument instead of the circular object itself.

Deterministic ("stable") safe stringify

  • Manipulating the input object either in a toJSON or the replacer function will not have any effect on the output. The output entirely relies on the shape the input value had at the point passed to the stringify function!

  • In case a circular structure is detected and the replacer is used it will receive the string [Circular] as the argument instead of the circular object itself.

A side effect free variation without these limitations can be found as well (safe-stable-stringify). It is also faster than the current implementation. It is still considered experimental due to a new and more complex implementation.

Benchmarks

Although not JSON, the Node.js util.inspect method can be used for similar purposes (e.g. logging) and also handles circular references.

Here we compare fast-safe-stringify with some alternatives: (Lenovo T450s with a i7-5600U CPU using Node.js 8.9.4)

fast-safe-stringify:   simple object x 1,121,497 ops/sec ±0.75% (97 runs sampled)
fast-safe-stringify:   circular      x 560,126 ops/sec ±0.64% (96 runs sampled)
fast-safe-stringify:   deep          x 32,472 ops/sec ±0.57% (95 runs sampled)
fast-safe-stringify:   deep circular x 32,513 ops/sec ±0.80% (92 runs sampled)

util.inspect:          simple object x 272,837 ops/sec ±1.48% (90 runs sampled)
util.inspect:          circular      x 116,896 ops/sec ±1.19% (95 runs sampled)
util.inspect:          deep          x 19,382 ops/sec ±0.66% (92 runs sampled)
util.inspect:          deep circular x 18,717 ops/sec ±0.63% (96 runs sampled)

json-stringify-safe:   simple object x 233,621 ops/sec ±0.97% (94 runs sampled)
json-stringify-safe:   circular      x 110,409 ops/sec ±1.85% (95 runs sampled)
json-stringify-safe:   deep          x 8,705 ops/sec ±0.87% (96 runs sampled)
json-stringify-safe:   deep circular x 8,336 ops/sec ±2.20% (93 runs sampled)

For stable stringify comparisons, see the performance benchmarks in the safe-stable-stringify readme.

Protip

Whether fast-safe-stringify or alternatives are used: if the use case consists of deeply nested objects without circular references the following pattern will give best results. Shallow or one level nested objects on the other hand will slow down with it. It is entirely dependant on the use case.

const stringify = require('fast-safe-stringify')

function tryJSONStringify (obj) {
  try { return JSON.stringify(obj) } catch (_) {}
}

const serializedString = tryJSONStringify(deep) || stringify(deep)

Acknowledgements

Sponsored by nearForm

License

MIT