🏯 ZenJSON
Transform custom types into JSON-compatible data.
Have you ever tried to do something like this:
JSON.stringify({ createdAt: new Date() });
This works but the date is converted into a string so when you JSON.parse
the result you don't get a Date
back.
ZenJSON allow you to transform you data into a new one that can safely be stringify
ed. Of course you can also do the reverse operation to get back your original data.
Here is a gist:
import { sanitize, restore } from 'https://deno.land/x/zenjson/mod.ts';
const data = {
createdAt: new Date(),
nested: {
array: [Infinity, undefined, NaN],
},
};
// sanitize your data before calling JSON.stringify
const str = JSON.stringify(sanitize(data));
// restore afer calling JSON.parse to get back the original data
const parsed = restore(JSON.parse(str));
assert(parsed.createdAt instanceof Date);
assert(parsed.nested.array[0] === Infinity);
Installation
# npm
npm install zenjson
# yarb
yarn add zenjson
You can also use this package in Deno / Recent Browser using ESM import like this:
import { sanitize, restore } from 'https://deno.land/x/zenjson/mod.ts';
// don't forget to fix the version, for example https://deno.land/x/zenjson@v1.0.0/mod.ts
// open https://deno.land/x/zenjson/mod.ts to find the latest one
Suported data type
By default ZenJSON supports the following types:
- Any data normally suported in JSON (string, number, boolean, null, object, array)
Date
- Special numbers:
Infinity
,-Infinity
andNaN
undefined
How does it works
When you call sanitize
, ZenJSON will traverse your data and match it against a list of custom types. When a value matches a type, it will replace the value into a tuple of two elements (the first element is the name of the type, the second is the sanitized value).
For example if you call sanitize(NaN)
it will produce the following result: ['number', 'NaN']
.
If you then call the restore
function with this tuple: restore(['number', 'NaN'])
it will return NaN
.
Note: Both sanitize
and restore
create a shallow copy, even if no change is made.
Custom types
You can provide your own custom types and even replace the default ones using the createSanitize
and createRestore
functions.
These functions take one argument: the list of supported types. The defaults supported types are exposed as defaultTypes
.
- The
sanitize
function correspond tocreateSanitize(defaultTypes)
- The
restore
function correspond tocreateRestore(defaultTypes)
If you use one of the create
function you probably want to add your own custom type:
import { createSanitize, defaultTypes } from 'https://deno.land/x/zenjson/mod.ts';
const sanitize = createSanitize([
// copy the default types
...defaultTypes,
// add your own type (keep reading to find out how to create this)
myCustomType,
]);
Creating a custom type
Note: If you use TypeScript you can use the CustomType
export to defined you types.
A custom type is an object with the following properties:
name
: This identify your type. It must be unique. The defaults types uses the following names:undefined
,number
,array
,date
.check
: A function that receive a value and returntrue
if it has the correct type.sanitize
: A function that receive the value and must return a JSON-compatible version of it (for example(date) => date.toISOString()
).restore
: A function that receive the sanitized value and must return the restored value (for example(dateStr) => parseISO(dateStr)
).
Here is an example of a custom type that handles BigInt:
import { createSanitize, createRestore, defaultTypes } from 'https://deno.land/x/zenjson/mod.ts';
const bigintType = {
name: 'bigint',
check: (val) => typeof val === 'bigint',
sanitize: (val) => val.toString(),
restore: (val) => BigInt(val),
};
const sanitize = createSanitize([...defaultTypes, bigintType]);
const restore = createRestore([...defaultTypes, bigintType]);
const data = { num: 123456789123456789123456n };
const sanitized = sanitize(data); // { num: ['bigint', '123456789123456789123456'] }
const restored = restore(sanitized); // { num: 123456789123456789123456n }
Note: The check
, sanitize
and restore
also receive a second parameter: ctx
. This is an object that contains some usefull data and functions.
Take a look a the types.ts
file to see how the defaults types are implemented.
By the way, each one of the default types are exported and can be used indivdually. They are four types:
dateType
(name:date
) handlesDate
objectsundefinedType
(name:undefined
) handlesundefined
valuesspecialNumberType
(name:number
) handlesNaN
,Infinity
and-Infinity
arrayType
(name:array
) handles special cases (see below)
arrayType
custom type
The You might be wonderring what happens if your data looks like one of the sanitized tuple, for example you might have something like this:
const data = {
keys: ['date', 'time'],
};
This object might cause an error because the restore
function will interpret the ['date', 'time']
as a sanitized value and will try to transform it into a date.
To solve this ZenJSON include the arrayType
in the list of default types. This type will tranform any array that looks like a sanitized tuple into a tuple ['array', __THE_ARRAY__]
.
In the example above the result of sanitize(data)
is:
const result = {
keys: ['array', ['date', 'time']],
};
Which is correctly restore
d into the original object.
Important: When you define your own types you should always include the arrayType
to avoid the problem desribed above.
Handling nested data
ZenJSON handles nested array and object but it will stop as soon as a custom type matches. If you want to sanitize data inside f your custom type, you can use the ctx.sanitize
and ctx.restore
.
Here is an example of a custom type for Set
:
const setType = {
name: 'set',
check: (val) => val && val instanceof Set,
sanitize: (val, ctx) => {
// extract list of values
const setValues = Array.from(set.values());
// use ctx.sanitize to sanitize items inside the set
return setValues.map((item) => ctx.sanitize(item));
},
restore: (val, ctx) => {
const restoredValues = val.map((item) => ctx.restore(item));
return new Set(restoredValues);
},
};