ohash and the small question of hashing a JavaScript object
# Simple object hashing, serialization and comparison utils.
At a glance
- What is it?
- ohash is a tiny unjs utility that serializes any JavaScript value to a stable string, hashes it with SHA-256 in Base64URL, compares two objects by their serialized form, and diffs them. It is useful for cache keys and change detection, and its own documentation warns that the serialization is a best effort and not built for security.
- Who is it for?
- ohash fits a JavaScript or TypeScript project that needs a stable identity for a value it cannot compare by reference, whether that is a cache key, a content address, or a change detector between two versions of an object. It is the wrong tool for anything security sensitive, because the documentation states the serialization is a best effort and that intentional collisions are always possible.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Four functions, and one of them is the whole design
ohash is a small package from the unjs organisation, described as simple object hashing, serialization and comparison utils. The public API is four functions plus one extra.
`hash(input)` hashes any JS value into a string. `serialize(input)` serializes any input value into a string for hashing. `digest(str)` hashes a string using the SHA-256 algorithm and encodes it in Base64URL format. `isEqual(obj1, obj2)` compares two objects using `===` and then falls back to comparing based on their serialized values. And `diff(obj1, obj2)` lives in a separate `ohash/utils` entry point.
The design logic is worth spelling out because it explains the whole package. `hash` is not a separate algorithm. The documentation says the input is first serialized and then hashed, so `hash(value)` is `digest(serialize(value))`. Everything else follows from that composition.
The two-function split is what makes the package useful beyond hashing. `serialize` gives you a stable, human readable structural string without any cryptographic step, which is what you want when you are comparing two objects and want to explain the difference. `hash` gives you a short fixed length digest, which is what you want when you are building a cache key or a content address. The output for a simple object looks like this:
import { hash } from "ohash";
// "g82Nh7Lh3CURFX9zCBhc5xgU0K7L0V1qkoHyRsKNqA4"
console.log(hash({ foo: "bar" }));The `digest` function is separately useful because it works on a string you already have:
import { digest } from "ohash";
// "f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk"
console.log(digest("Hello World!"));Base64URL rather than hex, so the digest is 43 characters for SHA-256 rather than 64, which matters when you are putting thousands of them in a URL or a database key.
The serialization is a best effort, and the package says so
There is an important note in the documentation, and it is marked as important rather than left as a footnote.
The `serialize` method uses best efforts to generate stable serialized values, however, it is not designed for security purposes. Keep in mind that there is always a chance of intentional collisions caused by user input.
That is a precise statement of the limitation, and it has two halves. The first is that the output is stable in practice, meaning the same logical value produces the same string across calls, which is what makes it usable for comparison. The second is that stability is not the same as injectivity. Two different values can produce the same string, and someone who controls the input can deliberately engineer that. So if you are using the hash as a security boundary, or as a signature over untrusted data, the guarantee you need is not there.
For the ordinary uses, which are cache invalidation, deduplication and change detection, this is a non-issue. Those uses need the same value to hash the same way, and they need different values to hash differently often enough to be useful, not always.
The word best efforts also implies something about the supported inputs. The documentation says the method serializes any input value, and the examples cover plain objects and nested objects, but it does not enumerate every type it handles or define the output for each. If your data includes class instances, typed arrays, dates, maps, sets or anything else with a non-obvious shape, the string you get back is the answer to how this particular implementation chooses to represent it, and you should check that against your expectations before you persist a hash of it.
diff returns structured changes, not a patch
The `diff` function is in its own entry point, `ohash/utils`, and it does something more useful than returning a boolean.
`diff(obj1, obj2)` compares two objects with nested serialization and returns an array of changes. The returned value is an array of diff entries with `$key`, `$hash`, `$value` and `$props`, and when logging, a string version of the changelog is displayed.
So the result is structured data, and the human readable form is a rendering of it. The documented example makes the shape clear:
import { diff } from "ohash/utils";
const createObject = () => ({
foo: "bar",
nested: {
y: 123,
bar: {
baz: "123",
},
},
});
const obj1 = createObject();
const obj2 = createObject();
obj2.nested.x = 123;
delete obj2.nested.y;
obj2.nested.bar.baz = 123;
const changes = diff(obj1, obj2);
// Removed `nested.y`
// Changed `nested.bar.baz` from `"123"` to `123`
// Added `nested.x`
console.log(changes.join("\n"));Three things are visible in that example. Additions, removals and changes are distinguished. A change is reported with both the old and the new value, and the type difference is preserved in the output, since `"123"` and `123` are reported as a change rather than as equal. And the nesting is walked, so a change three levels deep is reported with its full path.
That last property is what separates this from a shallow comparison. `isEqual` tells you whether two objects match, and `diff` tells you where they stopped matching, which is the question you usually have when a snapshot test fails or when you are deciding whether to re-run an expensive computation.
The `$hash` and `$props` fields on each entry are what let you build on top of it. `$hash` gives you the per key hash so you can tell a modified value from a moved one, and `$props` carries the properties of the change, so a consumer can react to a change differently depending on its kind.
Installing it takes one command and the import is ESM only
The install command is written for a package manager that picks itself, and the documentation is explicit that it works across the common ones:
# ✨ Auto-detect (npm, yarn, pnpm, bun or deno)
npx nypm i ohashThe import is a named ESM import, and there is a second entry point for the diff helper:
// ESM import
import { hash, serialize, digest, isEqual } from "ohash";
import { diff } from "ohash/utils";
// Dynamic import
const { hash, serialize, digest, isEqual } = await import("ohash");
const { diff } = await import("ohash/utils");The package manifest confirms the shape of that. The package type is `module`, `sideEffects` is `false`, and the exports map has three entries. The root resolves to `./dist/index.mjs`, the `./utils` subpath to `./dist/utils/index.mjs`, and the `./crypto` subpath is conditional: it resolves to `./dist/crypto/node/index.mjs` under the `node` condition and to `./dist/crypto/js/index.mjs` by default. That is the one piece of engineering in the packaging worth noticing, since it is how a SHA-256 implementation can come from Node's crypto module in a server and from a bundled JavaScript implementation in a browser without the caller doing anything.
The types entry is `./dist/index.d.mts`, and the published files array contains only `dist`, so nothing from the source tree or the tests ships to npm.
If you are working in a browser without a bundler, the documentation also gives a CDN route through esm.sh, with the same two import paths pointed at `https://esm.sh/ohash` and `https://esm.sh/ohash/utils`.
Contributing to the project is a pnpm workflow: enable Corepack with `corepack enable`, install dependencies with `pnpm install`, and run the interactive tests with `pnpm dev`.
The v1 to v2 migration broke hashes silently
The migration notes are the part of this repository most likely to cost you an afternoon, and the reason is that the breakage is invisible.
The note at the top says ohash v2 is the current release, and that for the previous API you should look at the v1 branch and the v2.0.1 release notes. The table then maps the old names to the new ones. `objectHash(value)`, which produced a stable digest string like `object:1:string:3:foo:...`, becomes `serialize(value)` for a stable string or `hash(value)` for a SHA-256 hash. And `hash(value)` still exists, but the implementation changed and the output format differs.
That second row is the dangerous one. A function that keeps its name, keeps its signature and keeps its purpose, and returns a different string, will not throw. It will return a value, your code will store it, and your cache keys or content addresses will all change at once. There is no error to catch.
The documentation is direct about the absence of a compatibility path: there is no v2 helper that reproduces the exact v1 `objectHash` string format. Use `serialize` when you need a stable structural string, and `hash` when you need a short digest.
So the migration guidance is not really about renaming. It is a choice between two different outputs, and you need to know which one you persisted before the upgrade. If you stored v1 `objectHash` strings anywhere durable, they cannot be regenerated, so a plan for that is part of the upgrade rather than a follow-up task.
The release history is consistent with a v2 that has been settled for a while: v1.1.6 and v2.0.11 were both published on 2025-03-04, within hours of each other, and v2.0.12 followed on 2026-08-14. The last push was on 2026-10-18, so the v2 line is the one being worked on.
Where a hash library is the wrong dependency
There are three situations where adding ohash is a mistake.
The first is security. The documentation is explicit that the serialization is not designed for security purposes and that intentional collisions are always possible from user input. If you are thinking of using a hash as a token, a signature, a content integrity check against an attacker, or a way to prove two documents are identical to a third party, this is the wrong tool and the reason is stated rather than inferred.
The second is when you already have a canonical serializer. A project that uses a structured clone format, a schema driven encoder, or a stable JSON stringifier already has a definition of what an identical value looks like. Duplicating that with a second hashing scheme means two answers to the same question, and the disagreements will show up as spurious diffs in whatever compares them.
The third is scale of need. If you are hashing one small object once, a few lines of `JSON.stringify` plus a crypto call is enough, and a dependency with a version migration in its history is more than the problem needs. The library earns its place when you hash many objects, or when you want the diff alongside the comparison.
There is also a subtlety around what a stable hash means across a long life. Because the implementation of the serialization can change between releases, and because the v1 to v2 change altered the output format, a hash you persist in a database is tied to the version that produced it. That is a reasonable constraint for a cache with a short life and a bad one for a content addressed store where the identifier has to survive years. The documentation does not promise format stability across versions, and the migration notes show that promise would not have been honest anyway.
Against a generic crypto library, the difference is the object handling
The alternative is to reach for the platform's own crypto, which in Node is `crypto` with `createHash('sha256')`, and to write the serialisation yourself.
The difference in approach is entirely in the part before the hash. The digest step is trivial, and any crypto library does it identically. The interesting work is turning an arbitrary JavaScript value into a deterministic string, and that is what ohash does. Plain `JSON.stringify` is not enough for a general value: it throws on circular structures, it omits `undefined` properties, it renders `Map` and `Set` as empty objects, it turns a `Date` into an ISO string that is convenient but not obviously the same identity, and its key order follows insertion order, so two objects that are equal in content can serialise differently depending on how they were built.
ohash's `isEqual` addresses the last of those directly. The documentation describes it as comparing two objects using `===` and then falling back to compare based on their serialized values, and the example is the case that breaks naive comparison:
import { isEqual } from "ohash";
// true
console.log(isEqual({ a: 1, b: 2 }, { b: 2, a: 1 }));Two objects with the same properties in a different order are equal, because the comparison goes through the serialised form rather than through key order.
The cost of the library is the dependency and the format commitment. What you gain is one consistent answer to the identity question across your whole codebase, and the diff on top of it. If you already have a serialisation convention, wrapping it in a single shared function is a perfectly good alternative, and it will not surprise you with a format change on upgrade.
The lineage is worth a note too. The documentation credits the object serialisation as originally based on puleos/object-hash by Scott Puleo, and the SHA-256 implementation as originally based on brix/crypto-js, which tells you the package is a focused rewrite rather than a from scratch piece of cryptography.
Editorial conclusion
ohash fits a JavaScript or TypeScript project that needs a stable identity for a value it cannot compare by reference, whether that is a cache key, a content address, or a change detector between two versions of an object. It is the wrong tool for anything security sensitive, because the documentation states the serialization is a best effort and that intentional collisions are always possible. Before you depend on it, verify three things: that the values you hash are the kind it supports, since the output format is a best effort rather than a canonical form, that a hash you persist stays stable across the version you upgrade to, because the v1 to v2 migration notes say the hash output format differs between versions, and that you are on v2, since the previous API lives on a separate branch. The code is MIT licensed, the current version is 2.0.12 from 2026-08-14, and the last push was on 2026-10-18.
Frequently asked questions
What does the ohash package do?
It provides simple object hashing, serialization and comparison utilities. `hash(input)` hashes any JavaScript value into a string, `serialize(input)` serializes a value into a stable string, `digest(str)` hashes a string with SHA-256 in Base64URL format, `isEqual` compares two objects, and `diff` returns an array of changes.
How do I install ohash?
Run `npx nypm i ohash`, which auto-detects npm, yarn, pnpm, bun or deno. The package is ESM only, with the main entry at `ohash` and the diff helper at `ohash/utils`, and the package manifest records pnpm as the package manager for development.
Is ohash safe to use for security purposes?
No. The documentation states that the `serialize` method uses best efforts to generate stable serialized values and is not designed for security purposes, and that there is always a chance of intentional collisions caused by user input. It is meant for comparison and caching, not for integrity guarantees.
What changed between ohash v1 and v2?
The v1 `objectHash(value)` became `serialize(value)` for a stable string or `hash(value)` for a SHA-256 hash, and while `hash(value)` still exists its implementation changed and its output format differs. There is no v2 helper that reproduces the exact v1 `objectHash` string format, so hashes persisted from v1 cannot be regenerated.
What does the diff function in ohash return?
It returns an array of diff entries with `$key`, `$hash`, `$value` and `$props`, describing removals, additions and changes with their nested paths. Logging the array shows a string version of the changelog, and it is imported from the `ohash/utils` entry point.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/unjs-ohash)Was this useful?