UNPKG

6.16 kBMarkdownView Raw
1# devalue
2
3Like `JSON.stringify`, but handles
4
5- cyclical references (`obj.self = obj`)
6- repeated references (`[value, value]`)
7- `undefined`, `Infinity`, `NaN`, `-0`
8- regular expressions
9- dates
10- `Map` and `Set`
11- `BigInt`
12- `ArrayBuffer` and Typed Arrays
13- custom types via replacers, reducers and revivers
14
15Try it out [here](https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239?version=3.49.0).
16
17## Goals:
18
19- Performance
20- Security (see [XSS mitigation](#xss-mitigation))
21- Compact output
22
23## Non-goals:
24
25- Human-readable output
26- Stringifying functions
27
28## Usage
29
30There are two ways to use `devalue`:
31
32### `uneval`
33
34This function takes a JavaScript value and returns the JavaScript code to create an equivalent value — sort of like `eval` in reverse:
35
36```js
37import * as devalue from 'devalue';
38
39let obj = { message: 'hello' };
40devalue.uneval(obj); // '{message:"hello"}'
41
42obj.self = obj;
43devalue.uneval(obj); // '(function(a){a.message="hello";a.self=a;return a}({}))'
44```
45
46Use `uneval` when you want the most compact possible output and don't want to include any code for parsing the serialized value.
47
48### `stringify` and `parse`
49
50These two functions are analogous to `JSON.stringify` and `JSON.parse`:
51
52```js
53import * as devalue from 'devalue';
54
55let obj = { message: 'hello' };
56
57let stringified = devalue.stringify(obj); // '[{"message":1},"hello"]'
58devalue.parse(stringified); // { message: 'hello' }
59
60obj.self = obj;
61
62stringified = devalue.stringify(obj); // '[{"message":1,"self":0},"hello"]'
63devalue.parse(stringified); // { message: 'hello', self: [Circular] }
64```
65
66Use `stringify` and `parse` when evaluating JavaScript isn't an option.
67
68### `unflatten`
69
70In the case where devalued data is one part of a larger JSON string, `unflatten` allows you to revive just the bit you need:
71
72```js
73import * as devalue from 'devalue';
74
75const json = `{
76 "type": "data",
77 "data": ${devalue.stringify(data)}
78}`;
79
80const data = devalue.unflatten(JSON.parse(json).data);
81```
82
83## Custom types
84
85You 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_:
86
87```js
88class Vector {
89 constructor(x, y) {
90 this.x = x;
91 this.y = y;
92 }
93
94 magnitude() {
95 return Math.sqrt(this.x * this.x + this.y * this.y);
96 }
97}
98
99const stringified = devalue.stringify(new Vector(30, 40), {
100 Vector: (value) => value instanceof Vector && [value.x, value.y]
101});
102
103console.log(stringified); // [["Vector",1],[2,3],30,40]
104
105const vector = devalue.parse(stringified, {
106 Vector: ([x, y]) => new Vector(x, y)
107});
108
109console.log(vector.magnitude()); // 50
110```
111
112If a function passed to `stringify` returns a truthy value, it's treated as a match.
113
114You can also use custom types with `uneval` by specifying a custom replacer:
115
116```js
117devalue.uneval(vector, (value, uneval) => {
118 if (value instanceof Vector) {
119 return `new Vector(${value.x},${value.y})`;
120 }
121}); // `new Vector(30,40)`
122```
123
124Note that any variables referenced in the resulting JavaScript (like `Vector` in the example above) must be in scope when it runs.
125
126## Error handling
127
128If `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`:
129
130```js
131try {
132 const map = new Map();
133 map.set('key', function invalid() {});
134
135 uneval({
136 object: {
137 array: [map]
138 }
139 });
140} catch (e) {
141 console.log(e.path); // '.object.array[0].get("key")'
142}
143```
144
145## XSS mitigation
146
147Say 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:
148
149```js
150const state = {
151 userinput: `</script><script src='https://evil.com/mwahaha.js'>`
152};
153
154const template = `
155<script>
156 // NEVER DO THIS
157 var preloaded = ${JSON.stringify(state)};
158</script>`;
159```
160
161Which would result in this:
162
163```html
164<script>
165 // NEVER DO THIS
166 var preloaded = {"userinput":"
167</script>
168<script src="https://evil.com/mwahaha.js">
169 "};
170</script>
171```
172
173Using `uneval` or `stringify`, we're protected against that attack:
174
175```js
176const template = `
177<script>
178 var preloaded = ${uneval(state)};
179</script>`;
180```
181
182```html
183<script>
184 var preloaded = {
185 userinput:
186 "\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fmwahaha.js'\\u003E"
187 };
188</script>
189```
190
191This, 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`:
192
193```js
194const value = (0, eval)('(' + str + ')');
195```
196
197## Other security considerations
198
199While `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.
200
201When using `eval`, ensure that you call it _indirectly_ so that the evaluated code doesn't have access to the surrounding scope:
202
203```js
204{
205 const sensitiveData = 'Setec Astronomy';
206 eval('sendToEvilServer(sensitiveData)'); // pwned :(
207 (0, eval)('sendToEvilServer(sensitiveData)'); // nice try, evildoer!
208}
209```
210
211Using `new Function(code)` is akin to using indirect eval.
212
213## See also
214
215- [lave](https://github.com/jed/lave) by Jed Schmidt
216- [arson](https://github.com/benjamn/arson) by Ben Newman. The `stringify`/`parse` approach in `devalue` was inspired by `arson`
217- [oson](https://github.com/KnorpelSenf/oson) by Steffen Trog
218- [tosource](https://github.com/marcello3d/node-tosource) by Marcello Bastéa-Forte
219- [serialize-javascript](https://github.com/yahoo/serialize-javascript) by Eric Ferraiuolo
220- [jsesc](https://github.com/mathiasbynens/jsesc) by Mathias Bynens
221- [superjson](https://github.com/blitz-js/superjson) by Blitz
222- [next-json](https://github.com/iccicci/next-json) by Daniele Ricci
223
224## License
225
226[MIT](LICENSE)