Ever needed a cache key from a complex API response, an idempotency key from a request body, or a way to detect identical payloads across services? If you reach for JSON.stringify plus a hash function, you'll discover edge cases you didn't account for: key order breaks your cache, circular objects crash your process, and BigInt throws before you even start.
Kokuin solves this with a single function: pass in any JSON-compatible value, get a deterministic SHA-256 hash back. No config, no options, no surprises.
The problem
JSON.stringify(value) + SHA-256(...) is the naive approach to hashing JSON values for cache keys, deduplication, or idempotency — but it breaks on key order, throws on circular references, and silently mis-handles types like undefined and BigInt. Most hashing libraries either operate on strings/blobs or require pulling in a heavy dependency tree.
What if you want a single function that takes any JSON-compatible value and returns a deterministic, cross-runtime hash — without worrying about key ordering, circular objects, or which crypto API is available?
Enter kokuin.
What is kokuin?
Kokuin is a zero-dependency TypeScript library (~200 LOC) that hashes JSON-compatible values deterministically. The entire API is a single function: hash(value).
import { hash } from 'kokuin'
hash({ a: 1, b: 2 })
hash({ b: 2, a: 1 })
// → same hash, key order doesn't matter
hash([1, 2, 3])
hash([3, 2, 1])
// → different hash — array order matters
Why not JSON.stringify + crypto?
| Problem |
JSON.stringify + crypto |
kokuin |
|---|---|---|
| Key ordering |
{b:2, a:1} ≠ {a:1, b:2}
|
Normalized — same hash |
| Circular references | Throws | Gracefully resolved |
undefined |
JSON.stringify({a: undefined}) → '{}'
|
Preserved as a value |
BigInt |
Throws | Supported |
-0 === 0 |
JSON.stringify(-0) → '-0'
|
Normalized to 0
|
| Cross-runtime | Depends on crypto.subtle vs createHash
|
Same pure JS SHA-256 everywhere |
How it works
Under the hood, kokuin builds a canonical binary representation of the value:
- Serialize the value to a canonical byte buffer — type tags (1 byte), numbers (Float64), strings (UTF-8 with length prefix), sorted keys for objects
- Hash the buffer with a hand-written SHA-256 (pure JS, zero dependencies, works on Node, Bun, Deno, and browsers)
// The type tags keep values distinct
hash(1) !== hash('1') // number ≠ string
hash(1n) !== hash(1) // BigInt ≠ number
hash(null) !== hash(undefined)
hash(false) !== hash(0)
Circular references
The one place kokuin deliberately goes beyond JSON:
const a: any = {}
a.self = a
hash(a) // does not throw — resolves the cycle
toJSON() support
Any object with a toJSON() method is hashed via its return value — the same contract JSON.stringify follows:
class Money {
constructor(private cents: number) {}
toJSON() { return { cents: this.cents } }
}
hash(new Money(500)) === hash({ cents: 500 }) // true
A guard prevents built-in types (like Date) that have toJSON on their prototype from sneaking through:
hash(new Date()) // Error: Cannot hash Date values
What it doesn't do
Kokuin intentionally rejects types where "identity" in JavaScript is unreliable:
hash(new Map()) // Error
hash(new Set()) // Error
hash(new Date()) // Error
hash(/abc/) // Error
hash(new Error()) // Error
hash(() => {}) // Error
hash(Symbol()) // Error
These aren't oversights. A class name survives minification only by luck; instanceof can fail across realms (iframes, workers, multiple copies of the package). Instead of silently producing a shaky hash, kokuin asks you to convert explicitly:
hash(Object.fromEntries(myMap)) // Map → object
hash([...mySet].sort()) // Set → sorted array
hash(myDate.toISOString()) // Date → string
hash({ message: err.message }) // Error → plain object
Why pure JS SHA-256?
Zero dependencies means zero node_modules overhead, zero polyfill concerns, and zero runtime-specific branches. The SHA-256 implementation fits in ~100 lines of plain bitwise operations. The same hash on Node, Bun, Deno, and every modern browser.
Use cases
- Cache keys — hash complex query parameters deterministically, regardless of how the object is constructed
- Deduplication — detect identical payloads across requests
- Idempotency — generate idempotency keys from request bodies
- Structural identity — compare deeply nested data structures by their hash
The name
"Kokuin" (刻印) means "engraving" or "stamp" in Japanese — a permanent, deterministic mark.
Try it
npm install kokuin
pnpm add kokuin
yarn add kokuin
GitHub: https://github.com/smokeeaasd/kokuin
npm: https://npmjs.com/package/kokuin
Top comments (0)