# devalue: serializing the JavaScript values JSON.stringify drops

> devalue is a small MIT-licensed serializer from the Svelte team that keeps cycles, Map, Set, BigInt, dates and custom classes intact. It is built for machine consumption, not for reading, and its own README warns that output is not stable across versions.

**sveltejs/devalue** — Gets the job done when JSON.stringify can't

- Repository: https://github.com/sveltejs/devalue
- Website: https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239
- Stars: 2,800 · Forks: 100
- Language: JavaScript
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/sveltejs-devalue

## What devalue fixes that JSON.stringify quietly loses

JSON.stringify throws on a cyclical reference and silently drops or rewrites a long list of values. devalue's README lists what it handles instead: cyclical references such as obj.self = obj, repeated references, undefined, Infinity, NaN, -0, regular expressions, dates, Map and Set, BigInt, ArrayBuffer and typed arrays, URL and URLSearchParams, Temporal, and promises through stringifyAsync. Custom classes are covered by replacers, reducers and revivers.

The audience is narrow and specific. This is for people moving JavaScript values between two JavaScript runtimes: server to browser, worker to main thread, or one process to another. It is not a data interchange format for other languages, and the README does not present it as one. The stated non-goals are human-readable output, stringifying functions, and stability of serialization mechanisms between versions. That third one matters more than it looks, and it is covered below.

The project is MIT licensed and the package declares "type": "module" with an engines field of node >=22.17, so it is ESM-only in practice and expects a recent Node.

## uneval, stringify and parse: three different outputs

devalue exposes three ways to turn a value into something transportable, and picking the wrong one is the most common mistake.

uneval returns JavaScript source. The README gives this example: devalue.uneval({ message: 'hello' }) produces '{message:"hello"}', and adding obj.self = obj produces '(function(){let a={};a.message="hello";a.self=a;return a}())'. That output is meant to be embedded in a script element or a generated module, and it needs no parser shipped alongside it. The trade-off is that you are generating code, so the README notes that any variables referenced in the result must be in scope when it runs.

stringify returns a compact string in a flat array format. A plain object becomes '[{"message":1},"hello"]', where the first element is a shape table and the rest are values. With a cycle it becomes '[{"message":1,"self":0},"hello"]', and parse rebuilds { message: 'hello', self: [Circular] }. Use this pair when evaluating JavaScript is not an option, which the README frames as the case of sending untrusted data from client to server.

stringifyAsync awaits promises inside the value and serializes their resolved results. The README states that its output format is identical to stringify, so parse and unflatten work on it unchanged. That is a deliberate design choice: one parser, two producers.

unflatten is the escape hatch for nesting. If devalued data is one field inside a larger JSON document, you can call JSON.parse on the outer document and then pass just that field to unflatten, reviving the inner value without touching the rest.

## Installing devalue and serializing a cycle

devalue is published on npm as devalue. The package is ESM ("type": "module" in package.json) and its engines field requires Node 22.17 or newer, so check that before installing.

```bash
npm install devalue
```

A first real use is the case JSON.stringify refuses outright. The README's own example builds an object that points at itself, serializes it, and parses it back.

```js
import * as devalue from 'devalue';

let obj = { message: 'hello' };
obj.self = obj;

let stringified = devalue.stringify(obj);
devalue.parse(stringified);
```

The stringified value is a compact array string, not something you would read comfortably. What you should see after parse is an object with a message property and a self property pointing back at the object itself. If you are targeting the browser instead of Node, the same import works from a bundler, and the README points to a REPL on svelte.dev for trying it without installing anything.

If your data is inside a larger JSON payload, revive only the relevant field:

```js
const data = devalue.unflatten(JSON.parse(json).data);
```

That call assumes json.data holds a string produced by devalue.stringify. Passing a plain JSON value to unflatten is not the intended use.

## Custom types need a reducer and a reviver, or nothing works

Class instances do not survive stringify on their own. You describe them with a reducer passed as the second argument to stringify, and rebuild them with a reviver passed to parse or unflatten. The README's Vector example is the clearest statement of the contract:

```js
const stringified = devalue.stringify(new Vector(30, 40), {
  Vector: (value) => value instanceof Vector && [value.x, value.y]
});

const vector = devalue.parse(stringified, {
  Vector: ([x, y]) => new Vector(x, y)
});
```

The rule is that a function passed to stringify is treated as a match when it returns a truthy value. Return false and the value falls through to normal serialization, which for a class instance means you get a plain object with the same enumerable properties and lose the prototype. There is no error and no warning. That silence is the failure mode to watch for: a missing reviver on the parse side produces a plain object where you expected a class, and the mistake usually surfaces much later as a method that is not a function.

For uneval there is a separate path. You pass a replacer whose second argument is a js tag, and you return a tagged template such as js`new Vector(${value.x},${value.y})`. The README notes that identifier-like words in the literal template strings, including nested templates, are reserved so generated variables cannot shadow your constructors or local bindings. That is a real piece of engineering, and it exists because uneval output is code that runs in a scope you do not fully control.

## The binary data trap: your whole ArrayBuffer goes on the wire

This is the part of the README most likely to cause an incident, and it deserves to be read twice.

For ordinary typed arrays and DataView, devalue serializes the entire backing ArrayBuffer, preserving shared views and byte offsets. The README says this explicitly includes bytes outside a subarray or subview. So if you take a 16-byte view into a 1 MB buffer, the serialized output can carry the whole megabyte, including whatever else lives in it. The README's guidance is blunt: only serialize these views if their entire backing store is safe to disclose, or copy the intended bytes first. It gives the copy as new Uint8Array(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)).

Node Buffers are handled differently and more carefully. Buffers are serialized as Uint8Arrays containing only the Buffer's visible bytes, and their backing stores are copied, because small Buffers can share an allocation pool holding unrelated data. Repeated references to the same Buffer are preserved, but distinct Buffers get separate backing stores.

The gap is in how a value is classified. The README states that if you explicitly pass buf.buffer, or build an ordinary typed array or DataView over a Node Buffer's backing store, those values cannot be identified as Node Buffers and fall under the whole-buffer rule. To copy a Buffer's contents into an independent typed array, the README says to use new Uint8Array(buf).

If your payloads contain views into larger buffers and any of those bytes are sensitive, this is a reason to copy before serializing, or to not use devalue for that field at all.

## Overriding operations when serialization must not run your code

By default, serializing a value can execute user code. The README lists the ways: getters and proxy traps fire during property reads, Object.prototype.toString consults a possibly getter-defined Symbol.toStringTag, and patched prototype methods such as Date.prototype.toISOString or Map.prototype[Symbol.iterator] are invoked.

The operations option lets you replace that introspection layer. Omitted members fall back to the defaults exported as defaultStringifyOperations, which the README says behave exactly as devalue always has. The README's example captures Date.prototype.toISOString up front and reads properties through descriptors so getters never run:

```js
const originalToISOString = Date.prototype.toISOString;

const stringified = devalue.stringify(value, undefined, {
  operations: {
    toISOString: (date) => originalToISOString.call(date),
    get: (object, key) => {
      const descriptor = Object.getOwnPropertyDescriptor(object, key);
      if (descriptor?.get) throw new Error(`refusing
```

The README cuts off mid-example there, so treat the snippet as a direction rather than a complete recipe. What it establishes is that this hook exists for deterministic or sandboxed runtimes, and that using it means you now own the correctness of every operation you override. That is a meaningful maintenance cost, and the README does not claim otherwise.

## Version coupling, licence and what you are signing up for

The README's non-goals include stability of serialization mechanisms between versions, with the parenthetical that stringifying with one version and parsing with another may break things. That is a deployment constraint, not a footnote. If a browser bundle and a server both touch devalued strings, they need to move together, which rules out long-lived cached payloads and mixed-version fleets unless you keep the format pinned on both ends.

Maintenance is current. The last push to the repository was on 2026-09-23, and the recent release line runs v6.0.0 on 2026-09-19, v6.0.1 on 2026-09-21 and v6.0.2 on 2026-09-22. The repository is not archived. Releases are managed with Changesets, and the prepublishOnly script runs the unit tests, the type check, the TypeScript build and publint, so a publish is gated on all four. The package ships src and types, and the build step is tsc.

The licence is MIT. That is permissive and standard for a library of this kind, but it is worth noting that MIT gives you no patent grant, unlike Apache-2.0. Nothing in the repository suggests that matters here; it is simply the difference between the two licences, and this is not legal advice.

Upgrade cost is where the non-goal bites. Because the format is explicitly not stable across versions, a dependency bump on one side of a boundary is a coordinated change, not a patch release you can merge and forget.

## Conclusion

Adopt devalue when your data crosses a boundary that JSON.stringify cannot survive: cycles, Map, Set, BigInt, undefined, or class instances you can describe with a reducer. Do not adopt it if the serialized string has to be read or diffed by a person, if you need to stringify functions, or if you plan to upgrade the two sides of the wire independently, because the README states that stringify and parse across different versions may break. Before shipping, verify three things: that your runtime is Node 22.17 or newer per the package engines field, that every custom type has both a reducer and a reviver, and whether any typed array you serialize carries bytes outside the subview you meant to send, since the README says the entire backing ArrayBuffer is written.

## FAQ

### What is devalue on npm used for?

It serializes JavaScript values that JSON.stringify cannot handle, including cyclical references, repeated references, undefined, Infinity, NaN, -0, regular expressions, dates, Map, Set, BigInt, ArrayBuffer, typed arrays, URL and URLSearchParams, and Temporal. It is aimed at moving values between JavaScript runtimes rather than at producing a format other languages can read.

### Does devalue handle cyclical references and Map or Set values?

Yes. The README lists cyclical references such as obj.self = obj and repeated references like [value, value], and it lists Map and Set among the supported types. parse rebuilds a cycle, which the README shows as { message: 'hello', self: [Circular] }.

### What is the difference between uneval and stringify in devalue?

uneval returns JavaScript source that recreates the value, such as '{message:"hello"}', and is meant for embedding in a script element or a generated module. stringify returns a compact array-format string that you revive with parse, and the README recommends it when evaluating JavaScript is not an option, for example when sending untrusted data from client to server.

### Which Node version does devalue require?

The package.json engines field specifies node >=22.17, and the package is ESM with "type": "module". Check your runtime before installing.

### Is devalue output stable between versions?

No. The README lists stability of serialization mechanisms between versions as a non-goal, stating that stringifying with one version and parsing with another may break. Keep both sides of a boundary on the same version.

## Sources

- [License: MIT](https://github.com/sveltejs/devalue/blob/main/LICENSE)
- [Project website](https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239)
- [README](https://github.com/sveltejs/devalue/blob/main/README.md)
- [Releases](https://github.com/sveltejs/devalue/releases)
- [sveltejs/devalue on GitHub](https://github.com/sveltejs/devalue)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sveltejs-devalue
