DEV Community

Lucas
Lucas

Posted on

kokuin — Deterministic Hashing for JSON Values in TypeScript

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Serialize the value to a canonical byte buffer — type tags (1 byte), numbers (Float64), strings (UTF-8 with length prefix), sorted keys for objects
  2. 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)
Enter fullscreen mode Exit fullscreen mode

Circular references

The one place kokuin deliberately goes beyond JSON:

const a: any = {}
a.self = a

hash(a) // does not throw — resolves the cycle
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
pnpm add kokuin
Enter fullscreen mode Exit fullscreen mode
yarn add kokuin
Enter fullscreen mode Exit fullscreen mode

GitHub: https://github.com/smokeeaasd/kokuin
npm: https://npmjs.com/package/kokuin

Top comments (0)