| # devalue | |
| Like `JSON.stringify`, but handles | |
| - cyclical references (`obj.self = obj`) | |
| - repeated references (`[value, value]`) | |
| - `undefined`, `Infinity`, `NaN`, `-0` | |
| - regular expressions | |
| - dates | |
| - `Map` and `Set` | |
| - `BigInt` | |
| - `ArrayBuffer` and Typed Arrays | |
| - `URL` and `URLSearchParams` | |
| - `Temporal` | |
| - custom types via replacers, reducers and revivers | |
| Try it out [here](https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239?version=3.49.0). | |
| ## Goals: | |
| - Performance | |
| - Security (see [XSS mitigation](#xss-mitigation)) | |
| - Compact output | |
| ## Non-goals: | |
| - Human-readable output | |
| - Stringifying functions | |
| ## Usage | |
| There are two ways to use `devalue`: | |
| ### `uneval` | |
| This function takes a JavaScript value and returns the JavaScript code to create an equivalent value — sort of like `eval` in reverse: | |
| ```js | |
| import * as devalue from 'devalue'; | |
| let obj = { message: 'hello' }; | |
| devalue.uneval(obj); // '{message:"hello"}' | |
| obj.self = obj; | |
| devalue.uneval(obj); // '(function(a){a.message="hello";a.self=a;return a}({}))' | |
| ``` | |
| Use `uneval` when you want the most compact possible output and don't want to include any code for parsing the serialized value. | |
| ### `stringify` and `parse` | |
| These two functions are analogous to `JSON.stringify` and `JSON.parse`: | |
| ```js | |
| import * as devalue from 'devalue'; | |
| let obj = { message: 'hello' }; | |
| let stringified = devalue.stringify(obj); // '[{"message":1},"hello"]' | |
| devalue.parse(stringified); // { message: 'hello' } | |
| obj.self = obj; | |
| stringified = devalue.stringify(obj); // '[{"message":1,"self":0},"hello"]' | |
| devalue.parse(stringified); // { message: 'hello', self: [Circular] } | |
| ``` | |
| Use `stringify` and `parse` when evaluating JavaScript isn't an option. | |
| ### `unflatten` | |
| In the case where devalued data is one part of a larger JSON string, `unflatten` allows you to revive just the bit you need: | |
| ```js | |
| import * as devalue from 'devalue'; | |
| const json = `{ | |
| "type": "data", | |
| "data": ${devalue.stringify(data)} | |
| }`; | |
| const data = devalue.unflatten(JSON.parse(json).data); | |
| ``` | |
| ## Custom types | |
| You can serialize and deserialize custom types by passing a second argument to `stringify` containing an object of types and their _reducers_, and a second argument to `parse` or `unflatten` containing an object of types and their _revivers_: | |
| ```js | |
| class Vector { | |
| constructor(x, y) { | |
| this.x = x; | |
| this.y = y; | |
| } | |
| magnitude() { | |
| return Math.sqrt(this.x * this.x + this.y * this.y); | |
| } | |
| } | |
| const stringified = devalue.stringify(new Vector(30, 40), { | |
| Vector: (value) => value instanceof Vector && [value.x, value.y] | |
| }); | |
| console.log(stringified); // [["Vector",1],[2,3],30,40] | |
| const vector = devalue.parse(stringified, { | |
| Vector: ([x, y]) => new Vector(x, y) | |
| }); | |
| console.log(vector.magnitude()); // 50 | |
| ``` | |
| If a function passed to `stringify` returns a truthy value, it's treated as a match. | |
| You can also use custom types with `uneval` by specifying a custom replacer: | |
| ```js | |
| devalue.uneval(vector, (value, uneval) => { | |
| if (value instanceof Vector) { | |
| return `new Vector(${value.x},${value.y})`; | |
| } | |
| }); // `new Vector(30,40)` | |
| ``` | |
| Note that any variables referenced in the resulting JavaScript (like `Vector` in the example above) must be in scope when it runs. | |
| ## Error handling | |
| If `uneval` or `stringify` encounters a function or a non-POJO that isn't handled by a custom replacer/reducer, it will throw an error. You can find where in the input data the offending value lives by inspecting `error.path`: | |
| ```js | |
| try { | |
| const map = new Map(); | |
| map.set('key', function invalid() {}); | |
| uneval({ | |
| object: { | |
| array: [map] | |
| } | |
| }); | |
| } catch (e) { | |
| console.log(e.path); // '.object.array[0].get("key")' | |
| } | |
| ``` | |
| ## XSS mitigation | |
| Say you're server-rendering a page and want to serialize some state, which could include user input. `JSON.stringify` doesn't protect against XSS attacks: | |
| ```js | |
| const state = { | |
| userinput: `</script><script src='https://evil.com/mwahaha.js'>` | |
| }; | |
| const template = ` | |
| <script> | |
| // NEVER DO THIS | |
| var preloaded = ${JSON.stringify(state)}; | |
| </script>`; | |
| ``` | |
| Which would result in this: | |
| ```html | |
| <script> | |
| // NEVER DO THIS | |
| var preloaded = {"userinput":" | |
| </script> | |
| <script src="https://evil.com/mwahaha.js"> | |
| "}; | |
| </script> | |
| ``` | |
| Using `uneval` or `stringify`, we're protected against that attack: | |
| ```js | |
| const template = ` | |
| <script> | |
| var preloaded = ${uneval(state)}; | |
| </script>`; | |
| ``` | |
| ```html | |
| <script> | |
| var preloaded = { | |
| userinput: | |
| "\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fmwahaha.js'\\u003E" | |
| }; | |
| </script> | |
| ``` | |
| This, along with the fact that `uneval` and `stringify` bail on functions and non-POJOs, stops attackers from executing arbitrary code. Strings generated by `uneval` can be safely deserialized with `eval` or `new Function`: | |
| ```js | |
| const value = (0, eval)('(' + str + ')'); | |
| ``` | |
| ## Other security considerations | |
| While `uneval` prevents the XSS vulnerability shown above, meaning you can use it to send data from server to client, **you should not send user data from client to server** using the same method. Since it has to be evaluated, an attacker that successfully submitted data that bypassed `uneval` would have access to your system. | |
| When using `eval`, ensure that you call it _indirectly_ so that the evaluated code doesn't have access to the surrounding scope: | |
| ```js | |
| { | |
| const sensitiveData = 'Setec Astronomy'; | |
| eval('sendToEvilServer(sensitiveData)'); // pwned :( | |
| (0, eval)('sendToEvilServer(sensitiveData)'); // nice try, evildoer! | |
| } | |
| ``` | |
| Using `new Function(code)` is akin to using indirect eval. | |
| ## See also | |
| - [lave](https://github.com/jed/lave) by Jed Schmidt | |
| - [arson](https://github.com/benjamn/arson) by Ben Newman. The `stringify`/`parse` approach in `devalue` was inspired by `arson` | |
| - [oson](https://github.com/KnorpelSenf/oson) by Steffen Trog | |
| - [tosource](https://github.com/marcello3d/node-tosource) by Marcello Bastéa-Forte | |
| - [serialize-javascript](https://github.com/yahoo/serialize-javascript) by Eric Ferraiuolo | |
| - [jsesc](https://github.com/mathiasbynens/jsesc) by Mathias Bynens | |
| - [superjson](https://github.com/blitz-js/superjson) by Blitz | |
| - [next-json](https://github.com/iccicci/next-json) by Daniele Ricci | |
| ## License | |
| [MIT](LICENSE) | |