The JSON Hash You Trust Is Lying to You

You’ve been hashing JSON objects for years. It works. Until it doesn’t.

There’s a quiet dread that creeps in when you realize your deterministic caching keys might be silently colliding on edge cases you never tested. The kind of bug that doesn’t throw an error—it just corrupts data, slowly, insidiously. And the worst part? Most developers think JSON hashing is a solved problem.

Treating JSON hashing as a simple ‘sort keys and stringify’ is like using a paperclip to lock your front door.

Sure, it works for the happy path. But what happens when your object graph has a circular reference? A self-referential object like {a: {b: {self: {self: ...}}}} isn’t JSON-serializable in the traditional sense. The naive approach—JSON.stringify—throws immediately. So clever engineers build custom hashers that detect cycles. They pat themselves on the back.

Then Kokuin appeared on Hacker News: a TypeScript library promising deterministic JSON hashing for any object, including those with circular references. It seemed like the holy grail. The implementation was elegant: track a stack of visited objects, and when you hit a cycle, write a special CIRCULAR byte. Genius, right?

Except one developer, reading the source, dropped a comment that should make you shiver:

“Does this mean that if a nested object holds a reference to itself, it will produce the same hash as if it held a reference to any of the objects it is nested in?”

Let that sink in. Two structurally different object graphs—one with a self-loop, another with a reference to a sibling node—could produce identical hashes. The library solved the ‘no crash’ problem, but introduced a new one: hash collisions based on structural ambiguity, not value equivalence.

This is the twist nobody’s talking about. We assume that eliminating crashes means we’ve eliminated bugs. But in the world of object hashing, every abstraction layer introduces a new way to lose information. Object identity—the very thing that makes JavaScript objects unique—is discarded the moment you flatten them into a deterministic sequence of bytes.

Your caching system, your state deduplication, your memoization library—all of them are quietly trusting a hash that might be lying to you.

Kokuin is a brilliant attempt. It’s not the problem—the problem is the fundamental tension between JavaScript’s dynamic, reference-based object graphs and the mathematical desire for a universal deterministic hash. You can’t have both without accepting trade-offs. The question is: are you aware of which trade-offs you’re making?

Most developers aren’t. They ship code that runs on untested edge cases and call it a day. The real vulnerability isn’t in the library—it’s in our collective assumption that ‘deterministic’ means ‘safe.’

So the next time you reach for a JSON hasher, ask yourself: am I hashing the value, or am I hashing the shape? And if the shape has a loop, what am I really losing?

Because the hash that works for every object is the hash that works for none.

FAQ

Q: Isn't JSON.stringify with a replacer enough to handle circular references?

A: No. JSON.stringify's replacer can prevent crashes by ignoring or replacing circular references, but it does not guarantee deterministic hashing across different object graphs. The same circular structure may produce different hash outputs depending on the replacer logic, and collisions are still possible when you flatten identity.

Q: What does this mean for my caching system in practice?

A: If you use object hashing for cache keys, a collision from a circular reference or sibling reference could cause two different computations to return the same cached result. This is a silent data corruption bug—your app won't crash, but it may serve stale or incorrect data. You need to thoroughly test your hashing with all possible object graph shapes, not just the typical ones.

Q: Isn't the real solution to avoid circular references altogether?

A: In an ideal world, yes. But real-world applications—especially those with complex state graphs, bidirectional relationships, or mutable data—often have circular references by design. Avoiding them may force you to restructure your data model in ways that are less natural. A better approach is to use a hash that preserves object identity, such as structural hashing with a separate mapping of object references, rather than just flattening the graph.

📎 Source: View Source