Cross-platform cryptographic hashing of EDN data structures for Clojure and ClojureScript.
hasch produces deterministic, content-based hashes for EDN values. Commutative data structures (maps, sets, records) hash identically regardless of iteration order, preserving Clojure value semantics. Hashes are consistent across JVM and JavaScript runtimes.
Two hashing modules are provided with different strengths:
hasch.core — string-based encoding with transparent structural hashing via HashRef. Replace any subtree with its HashRef and the parent hash stays identical — true merkle-style incremental hashing. Best for content-addressed storage, large nested structures, and cases where subtrees change independently.hasch.fast — binary-encoded streaming digest, ~6-14x faster per-value throughput. Best for hashing many independent values (UUID generation, deduplication, signatures).Both modules support all EDN types and incognito record serialization. They produce different hash values and should not be mixed for the same data.
Add to deps.edn:
org.replikativ/hasch {:mvn/version "0.3.97"}
(require '[hasch.core :as hc])
;; Generate UUID-5 from any EDN value
(hc/uuid {:name "Alice" :age 30})
;=> #uuid "..."
;; Transparent HashRef — same hash with or without:
(= (hc/uuid {:data [1 2 3]})
(hc/uuid {:data (hc/hash-ref [1 2 3])})) ;=> true
(hc/edn-hash [1 2 3])
;=> (120 75 53 ...) ;; seq of unsigned bytes
(hc/b64-hash "hello")
;=> "..."
(require '[hasch.fast :as hf])
;; Same API, ~6-14x faster, different hash values
(hf/uuid {:name "Alice" :age 30})
;=> #uuid "2c6ea7f3-..."
;; Random UUID-4
(hf/uuid)
;=> #uuid "a27dfbb9-..."
;; Raw hash bytes
(hf/edn-hash [1 2 3])
;=> #object["[B" ...]
;; Hex string / Base64
(hf/hash->str (hf/edn-hash "hello"))
(hf/b64-hash "hello")
;; SHA-256 variant (faster, still cryptographically secure)
(hf/sha256-uuid {:data "value"})
;; Sequential UUIDs (time-prefixed, sortable)
(hf/squuid)
| hasch.core | hasch.fast | |
|---|---|---|
| Encoding | String (pr-str of numbers) | Binary (direct byte layout) |
| Digest | Intermediate byte arrays per value | Streaming (update MessageDigest in-place) |
| Per-value speed | Baseline | ~6-14x faster |
| Structural hashing | Transparent HashRef | — |
| Best for | Content-addressed stores, merkle trees, incremental rehashing | UUID generation, deduplication, high-throughput hashing |
For large nested structures where subtrees change independently, hasch.core with HashRef can be faster end-to-end than hasch.fast, because unchanged subtrees are never re-traversed.
hasch.core supports transparent merkle-style structural hashing via HashRef. A HashRef caches the coercion output of a value so that when it appears inside a larger structure, it produces the same hash as the original value — no re-traversal needed.
(require '[hasch.core :as hc])
;; These produce identical hashes:
(hc/uuid {:data [1 2 3]})
(hc/uuid {:data (hc/hash-ref [1 2 3])}) ;; same! transparent substitution
;; Even edn-hash of a HashRef equals the original:
(= (hc/edn-hash x) (hc/edn-hash (hc/hash-ref x))) ;; true for all x
This is useful for content-addressed storage where you want to:
;; Hash subtrees once, reuse in parent structures
(def ref-a (hc/hash-ref {:name "Alice" :scores [1 2 3]}))
(def ref-b (hc/hash-ref {:name "Bob" :scores [4 5]}))
(hc/uuid {:users [ref-a ref-b]})
;; When sub-a changes, only rehash the changed subtree:
(def ref-a' (hc/hash-ref {:name "Alice" :scores [1 2 3 4]}))
(hc/uuid {:users [ref-a' ref-b]}) ;; ref-b reused as-is
Note: HashRef is only available in hasch.core. The streaming architecture of hasch.fast does not support transparent substitution.
Records are hashed via incognito, which normalizes JVM class names to a platform-neutral format (my.ns.MyRecord → my.ns/MyRecord). This ensures records hash identically across JVM and ClojureScript.
(require '[hasch.fast :as hf])
;; Records with default serialization work automatically
(defrecord Person [name age])
(hf/uuid (->Person "Alice" 30))
;; Custom write-handlers for non-default serialization
(hf/edn-hash (->Person "Alice" 30)
hf/sha512-message-digest
{'my.ns.Person (fn [r] [(:name r)])})
hash?Clojure's built-in hash is optimized for speed in data structures (murmur3, 32-bit) and intentionally trades collision resistance for performance. This is fine for hash maps but unacceptable for content-addressed systems where collisions could corrupt data. hasch uses SHA-512, providing cryptographic collision resistance suitable for distributed systems and untrusted environments.
Extend hasch.fast/PStreamHash (or hasch.benc/PHashCoercion for hasch.core) to add hashing for your own types. However, in most cases it is simpler to make your type serializable via incognito.
Your implementation must satisfy the equality relation:
(= a b) ⟹ (= (edn-hash a) (edn-hash b))(not= a b) ⟹ (not= (edn-hash a) (edn-hash b))# Run JVM tests
clj -M:test
# Run CLJS tests (requires Node.js)
npx shadow-cljs compile node-test
# Build JAR
clj -T:build jar
# Install locally
clj -T:build install
# Deploy to Clojars
clj -T:build deploy
hasch.fast module with binary streaming digest (~6-14x speedup). Add HashRef for merkle-style structural hashing. Add incognito support to hasch.fast.Copyright © 2014-2026 Christian Weilbach and contributors
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
Can you improve this documentation? These fine people already did:
Christian Weilbach, Timo Kramer & Antonio Nuno MonteiroEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |