seroval
Stringify JS values
Install
npm install --save seroval
yarn add seroval
pnpm add seroval
Usage
import { serialize } from 'seroval';
const object = {
number: [Math.random(), -0, NaN, Infinity, -Infinity],
string: ['hello world', '<script>Hello World</script>'],
boolean: [true, false],
null: null,
undefined: undefined,
bigint: 9007199254740991n,
array: [,,,], // holes
regexp: /[a-z0-9]+/i,
date: new Date(),
map: new Map([['hello', 'world']]),
set: new Set(['hello', 'world']),
};
// self cyclic references
// recursive objects
object.self = object;
// recursive arrays
object.array.push(object.array);
// recursive maps
object.map.set('self', object.map);
// recursive sets
object.set.add(object.set);
// mutual cyclic references
object.array.push(object.map);
object.map.set('mutual', object.set);
object.set.add(object.array);
const result = serialize(object);
console.log(result);
Output (as a string):
((h,j,k,m)=>(m={number:[0.28952097444015235,-0,NaN,1/0,-1/0],string:["hello world","\x3Cscript>Hello World\x3C/script>"],boolean:[!0,!1],null:null,undefined:void 0,bigint:9007199254740991n,array:h=[,,,,j=new Map([["hello","world"],["mutual",k=new Set(["hello","world"])]])],regexp:/[a-z0-9]+/i,date:new Date("2023-03-22T02:53:41.129Z"),map:j,set:k},h[3]=h,j.set("self",j),k.add(k).add(h),m.self=m,m))()
// Formatted for readability
((h, j, k, m) => (m = {
number: [0.28952097444015235, -0, NaN, 1 / 0, -1 / 0],
string: ["hello world", "\x3Cscript>Hello World\x3C/script>"],
boolean: [!0, !1],
null: null,
undefined: void 0,
bigint: 9007199254740991n,
array: h = [, , , , j = new Map([
["hello", "world"],
["mutual", k = new Set(["hello", "world"])]
])],
regexp: /[a-z0-9]+/i,
date: new Date("2023-03-22T02:53:41.129Z"),
map: j,
set: k
}, h[3] = h, j.set("self", j), k.add(k).add(h), m.self = m, m))()
Mutual cyclic example
import { serialize } from 'seroval';
const a = new Map([['name', 'a']]);
const b = new Map([['name', 'b']]);
const c = new Map([['name', 'c']]);
const d = new Map([['name', 'd']]);
c.set('left', a);
d.set('left', a);
c.set('right', b);
d.set('right', b);
a.set('children', [c, d]);
b.set('children', [c, d]);
const result = serialize({ a, b, c, d });
console.log(result);
Output (as a string):
((h,j,k,m,o,q)=>(q={a:h=new Map([["name","a"],["children",[j=new Map([["name","c"],["right",o=new Map([["name","b"],["children",k=[,m=new Map([["name","d"]])]]])]]),m]]]),b:o,c:j,d:m},j.set("left",h),k[0]=j,m.set("left",h).set("right",o),q))()
// Formatted
((h, j, k, m, o, q) => (q = {
a: h = new Map([
["name", "a"],
["children", [j = new Map([
["name", "c"],
["right", o = new Map([
["name", "b"],
["children", k = [, m = new Map([
["name", "d"]
])]]
])]
]), m]]
]),
b: o,
c: j,
d: m
}, j.set("left", h), k[0] = j, m.set("left", h).set("right", o), q))()
Deserialization
import { serialize, deserialize } from 'seroval';
const value = undefined;
console.log(deserialize(serialize(value)) === value);
JSON
serialize
and deserialize
is great for server-to-client communication, but what about the other way? serialize
may cause an RCE if used as a payload for requests. seroval
includes toJSON
and fromJSON
as an alternative form of serialization.
First example above outputs the following JSON
import { toJSON } from 'seroval';
// ...
const result = toJSON(object);
console.log(JSON.stringify(result));
{"t":{"t":16,"i":0,"d":{"k":["number","string","boolean","null","undefined","bigint","array","regexp","date","map","set","self"],"v":[{"t":15,"i":1,"l":5,"a":[{"t":0,"s":0.4350045546286634},{"t":5},{"t":8},{"t":6},{"t":7}]},{"t":15,"i":2,"l":2,"a":[{"t":1,"s":"hello world"},{"t":1,"s":"\\x3Cscript>Hello World\\x3C/script>"}]},{"t":15,"i":3,"l":2,"a":[{"t":2,"s":true},{"t":2,"s":false}]},{"t":3},{"t":4},{"t":9,"s":"9007199254740991"},{"t":15,"i":4,"l":5,"a":[null,null,null,{"t":10,"i":4},{"t":14,"i":5,"d":{"k":[{"t":1,"s":"hello"},{"t":1,"s":"self"},{"t":1,"s":"mutual"}],"v":[{"t":1,"s":"world"},{"t":10,"i":5},{"t":13,"i":6,"l":4,"a":[{"t":1,"s":"hello"},{"t":1,"s":"world"},{"t":10,"i":6},{"t":10,"i":4}]}],"s":3}}]},{"t":12,"i":7,"c":"[a-z0-9]+","m":"i"},{"t":11,"i":8,"s":"2023-03-22T02:55:33.504Z"},{"t":10,"i":5},{"t":10,"i":6},{"t":10,"i":0}],"s":12}},"r":0,"i":true,"f":8191,"m":[4,5,6,0]}
Then you can feed it to fromJSON
:
import { fromJSON } from 'seroval';
const revived = fromJSON(result);
Alternatively, if you want to compile the JSON output to JS (like deserialize
), you can use compileJSON
import { compileJSON, deserialize } from 'seroval';
const code = compileJSON(result);
const revived = deserialize(code);
Promise serialization
seroval
allows Promise serialization through serializeAsync
and toJSONAsync
.
import { serializeAsync } from 'seroval';
const value = Promise.resolve(100);
const result = await serializeAsync(value); // "Promise.resolve(100)"
console.log(await deserialize(result)); // 100
Note
seroval
can only serialize the resolved value and so the output will always be usingPromise.resolve
. If the Promise fulfills with rejection, the rejected value is thrown before serialization happens.
Serializable references
There are values that has no way to be serializable at all, i.e. functions, but usually in an isomorphic code, functions can exist on both client and server-side. What if we can serialize these functions in such a way we can refer to their counterparts?
seroval
has createReference
that you can use to map user-defined strings to their references.
import { createReference } from 'seroval';
const thisIsAnIsomorphicFunction = createReference(
// This is (ideally) a unique identifier
// that is used to map the serialized value
// to its actual reference (and vice versa)
'my-function',
() => {
// Ideally this function should exist on both
// server and client, but we want to add the ability
// to serialize and deserialize this reference on
// both sides
}
);
// we can now serialize this
const serialized = toJSON(thisIsAnIsomorphicFunction); // or any of the serializer
thisIsAnIsomorphicFunction === fromJSON(serialized); // true
Note It can only accept objects, functions and symbols and it doesn't actually serialize their values but only the string you used to identify the reference
Supports
The following values are the only values accepted by seroval
:
- Exact values
NaN
Infinity
-Infinity
-0
- Primitives
number
string
boolean
null
undefined
bigint
Array
+ holesObject
RegExp
Date
Map
Set
Object.create(null)
ArrayBuffer
DataView
TypedArray
Int8Array
Int16Array
Int32Array
Uint8Array
Uint16Array
Uint32Array
Uint8ClampedArray
Float32Array
Float64Array
BigInt64Array
BigUint64Array
Error
AggregateError
EvalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
Promise
(withserializeAsync
andtoJSONAsync
)Iterable
- Well-known symbols
- Web API
- Cyclic references (both self and mutual)
- Isomorphic references (a reference that exist on both the serializer and deserializer side)
Compat
serialize
, serializeAsync
, toJSON
and toJSONAsync
can accept a { disabledFeatures: number }
option. The disabledFeatures
defines how the output code would look like when serialized by serialize
, serializeAsync
and compileJSON
.
import { serialize, Feature } from 'seroval';
const y = Object.create(null);
y.self = y;
y.example = 'Hello World';
function serializeWithTarget(value, disabledFeatures) {
const result = serialize(value, {
disabledFeatures,
});
console.log(result);
}
serializeWithTarget(y, Feature.ArrowFunction | Feature.ObjectAssign);
serializeWithTarget(y, 0);
(function(h){return (h=Object.create(null),h.self=h,h.example="Hello World",h)})()
(h=>(h=Object.assign(Object.create(null),{example:"Hello World"}),h.self=h,h))()
disabledFeatures
uses bit flags for faster checking, so if you need to disable multiple features, you can use the bitwise OR symbol (|
).
Here's an ES2017
flag:
import { serialize, Feature } from 'seroval';
const ES2017FLAG =
Feature.AggregateError // ES2021
| Feature.BigInt // ES2020
| Feature.BigIntTypedArray // ES2020;
serialize(myValue, {
disabledFeatures: ES2017FLAG,
})
By default, all feature flags are enabled. The following are the feature flags and their behavior when disabled:
AggregateError
- Compiles down to
Error
instead.
- Compiles down to
ArrayPrototypeValues
- Used for
Iterable
, usesSymbol.iterator
instead.
- Used for
ArrowFunction
- Uses function expressions for top-level and for deferred
Promise
values - method shorthands (if
MethodShortand
is not set) or function expressions forIterable
.
- Uses function expressions for top-level and for deferred
BigInt
- Throws when attempted to use, includes
BigIntTypedArray
- Disables use of
BigInt
,BigInt64Array
andBigUint64Array
- Throws when attempted to use, includes
ErrorPrototypeStack
- Skipped when detected.
- Affects both
Error
andAggregateError
Map
- Throws when attempted to use.
- Disables serialization of
Map
MethodShorthand
- Uses function expressions instead.
- Only affects
Iterable
ObjectAssign
- Uses manual object assignments instead.
- Affects
Iterable
,Error
,AggregateError
andObject.create(null)
Promise
- Throws when attempted to use in
serializeAsync
andtoJSONAsync
. - Disables serialization of
Promise
- Throws when attempted to use in
Set
- Throws when attempted to use.
- Disables serialization of
Set
Symbol
- Throws when attempted to use.
- This disables serialization of well-known symbols and
Iterable
.
TypedArray
- Throws when attempted to use.
- Disables serialization of
TypedArray
BigIntTypedArray
- Throws when attempted to use
- Also throws if
BigInt
is disabled. - Disables serialization of
BigInt64Array
andBigUint64Array
WebAPI
Sponsors
License
MIT © lxsmnsyc