devalue publishes its src directory and promises nothing across versions
Gets the job done when JSON.stringify can't
At a glance
- What is it?
- A serializer for the values JSON cannot represent, with three entry points that differ in whether the output is code or data, an operations override for runtimes that cannot let a getter fire, and a documented warning that a typed array serialises its entire backing buffer. The packaging underneath is one line in the manifest: the export map points at src/index.js.
- Who is it for?
- devalue earns its place when your data genuinely contains cycles, repeated references, dates, Maps or BigInts, because JSON.stringify throws or silently mangles them. Choose the entry point by whether the output will be evaluated: uneval for output you control, which goes into a script tag or a generated module, and stringify with parse when the data crosses a trust boundary, which is what the security note on cross site payloads is about.
- 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 4 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- sveltejs/devalue2,812 ★ · +8
| Repository | 2026-10-04 | 2026-11-14 |
|---|---|---|
| sveltejs/devalue | 2.8k | 2.8k |
8 Stars in the last 41 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The published package is the source directory
The export map has three entries for the package root and none of them points at a bundle. Types resolve to ./types/types.d.ts, and both the import condition and the default condition resolve to ./src/index.js. The files array ships exactly two directories, src and types. So a consumer installs the source and executes it directly, with no distribution build step in between. The build script is consistent with that: it runs tsc and nothing else, which emits the declaration file the types entry needs and leaves the JavaScript alone. Two more manifest details matter for that arrangement. sideEffects is false, so a bundler may drop unreferenced exports, and type is module, so the entry is an ES module rather than a CommonJS one with a default interop. The stated engine floor is Node 22.17, which is a recent number for a library that ships unbundled source, and the package manager is pinned by name and integrity hash rather than left to a range.
Nothing guarantees a format that survives a version bump
The non-goals list has three entries, and the third one is the one to plan around: stability of serialization mechanisms between versions, with the parenthesis spelling out the consequence. If you stringify with one version and parse with another, things may break. There is no migration guarantee and no compatibility window described, because the format is treated as an implementation detail of a pair of functions that are always deployed together. That rules out a whole category of use: you cannot use this to write a value to disk, to a cache that outlives a deploy, or to a column in a database you will read next year, unless you pin the library version on both sides for as long as the data lives. The other two non-goals are narrower. Human readable output is explicitly not a goal, which is consistent with a format that references its values by index, and functions are not stringified at all, so a closure in your object graph disappears rather than erroring.
uneval emits code, which is why stringify exists
Two of the three entry points are deliberately different in kind. uneval takes a value and returns JavaScript source that recreates it, described as eval in reverse, and its result is meant for embedding in a script element or a generated module. stringify and parse are the analogous pair to JSON.stringify and JSON.parse, and the guidance is to use them when evaluating JavaScript is not an option, with sending untrusted data from client to server given as the example. That sentence is the security story in one line: one function's output is code, and code that reaches a page is a cross site scripting vector. The stated goals list performance, security with a pointer to an XSS mitigation section, and compact output, in that order. The compactness claim is visible in the examples, where a plain object becomes a short array of values and an object with a self reference becomes a structure that points at its own index instead of recursing.
A typed array serialises its entire backing buffer
This is the sharpest warning in the file and it sits under binary data. Node Buffer values are handled conservatively: they are serialized as Uint8Array containing only the Buffer's visible bytes, and their backing stores are copied, because small Buffers can share an allocation pool that contains unrelated and sensitive data. Ordinary typed arrays and DataViews get the opposite treatment. For those, the entire backing ArrayBuffer is serialized, preserving shared views and byte offsets, and that includes bytes outside the subarray or subview. The instruction is blunt about it: only serialize these views if their entire backing store is safe to disclose, or copy the intended bytes first. The copy is one line:
const copy = new Uint8Array(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
const serialized = devalue.stringify(copy);Two more cases slip past the Buffer protection. Passing buf.buffer explicitly does it, and so does creating an ordinary typed array or DataView over a Buffer's backing store, because in both cases the value can no longer be identified as a Node Buffer. So a value that started life as a safe Buffer can serialise as an unsafe one purely by being wrapped.
Reducers match on truthiness and are tried in turn
Custom types are registered by passing a second argument to stringify, an object of type names to reducers, and a matching second argument to parse or unflatten holding revivers. The reducer example reduces a Vector to its two coordinates, and the output puts the type name in the first slot with the reduced payload in the second. The rule for matching is one sentence: if a function passed to stringify returns a truthy value, it is treated as a match. Truthiness, not identity and not a type tag, is the test, and the reducers are consulted against values until one returns something. That has two consequences worth knowing before you register a large map of types. A reducer that returns a falsy value for a value it does not handle falls through to the next one, which is the intended behaviour, but it also means a reducer that accidentally returns 0 or an empty string will be treated as not matching. And a reducer cannot express a deliberate null result without falling through.
Serializing can run your code unless you replace the operations
Every introspection the serializer performs on a value goes through an operations interface, and the operations option replaces any part of it. Omitted members fall back to the defaults, exported as defaultStringifyOperations, which behave as devalue always has. The situation this exists for is stated plainly: by default, serializing a value can execute user code. Getters and proxy traps fire during property reads, the Object.prototype.toString consult can see a getter defined Symbol.toStringTag, and patched prototype methods such as Date.prototype.toISOString or the Map iterator are invoked. So serializing an object from another realm or from a hostile input is not a passive operation. The documented replacement is a set of implementations based on captured intrinsics and property descriptors, one that uses a saved reference to the original method and one that reads through descriptors so a getter is never invoked and can throw instead. The same override is what makes serialization deterministic inside a sandbox.
npm test and npm run test:unit are the same command
Two script entries are byte identical: test is vitest run --project unit and test:unit is vitest run --project unit. One of them is redundant, and which one tooling reaches for is anyone's guess. More interesting is the project selector. Passing --project unit means the default test command covers one named project out of whatever set vitest.config.js declares, so anything else configured there is not run by npm test. The prepublish path compensates by being explicit about its own sequence: it runs the test command, then a types check with tsc in no emit mode, then the build, then publint. Formatting is separate again, with check:format running prettier in check mode and format running it in write mode, so the publish gate never rewrites files. The last push to the default branch is dated 23 September 2026, and three releases landed in the four days before it.
Benchmarks run with a V8 flag Node does not enable by default
Both benchmark entries pass --allow-natives-syntax to node explicitly: one runs benchmarking/run.js and the other runs a comparison entry point under benchmarking/compare. That flag exposes V8 internals to JavaScript and is not on by default, so a benchmark cannot be started with a plain node invocation and the scripts have to carry it themselves. The benchmarking directory sits at the repository root rather than under a test folder, which tells you the performance goal is measured on a schedule rather than asserted in a unit test. The typescript dev dependency is on the 7 line while vite and vitest are pinned to exact versions with no range at all, and a changeset directory with a version and publish script pair sits alongside the changelog, which is how three releases inside four days produced consistent version numbers rather than manual edits.
Editorial conclusion
devalue earns its place when your data genuinely contains cycles, repeated references, dates, Maps or BigInts, because JSON.stringify throws or silently mangles them. Choose the entry point by whether the output will be evaluated: uneval for output you control, which goes into a script tag or a generated module, and stringify with parse when the data crosses a trust boundary, which is what the security note on cross site payloads is about. Two things to check before you adopt it. There is no stability guarantee between versions, so a value written by one release and read by another may break, which makes it unsuitable for data at rest. And before you serialise anything typed, check what the view covers: an ordinary typed array or DataView brings its whole ArrayBuffer with it, bytes outside the subarray included, while a Node Buffer is copied down to its visible bytes. The audit you want is whether that buffer holds anything you did not mean to send.
Frequently asked questions
What does devalue handle that JSON.stringify cannot?
Cyclical and repeated references, undefined, Infinity, NaN and negative zero, regular expressions, dates, Map and Set, BigInt, ArrayBuffer and typed arrays, URL and URLSearchParams, Temporal, custom types through replacers, reducers and revivers, and promises via stringifyAsync. Functions are not serialized, and human readable output is an explicit non-goal.
Can devalue parse data written by an older version?
Not reliably, and the file says so. Stability of serialization mechanisms between versions is listed as a non-goal, with the note that stringifying with one version and parsing with another may break. Treat the format as belonging to a pair of functions deployed together rather than as a storage format.
Does devalue leak bytes from a typed array's buffer?
For an ordinary typed array or DataView it will, because the entire backing ArrayBuffer is serialized, including bytes outside the subarray or subview. Node Buffers are the exception: only the visible bytes are serialized and the backing store is copied, since small Buffers can share a pool. Copy the intended bytes into a new Uint8Array first if the buffer holds anything sensitive.
When should I use devalue.uneval instead of stringify?
Use uneval when you want the most compact output and no parsing code needs to be shipped, for example when embedding the result in a script element or a generated JavaScript module. Use stringify with parse when evaluating JavaScript is not an option, which is the case the file names for sending untrusted data from client to server.
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/sveltejs-devalue)Star history chart for your README
[](https://hysenlabs.com/projects/sveltejs-devalue)Was this useful?