Next JavaScript Object Notation
Because JSON is awesome, but...
JSON is awesome mainly for two reasons:
- it offers an easy way to serialize and deserialize complex data;
- a valid JSON encoded string can be pasted in a JavaScript source file, a really awesome feature while developing / debugging.
... but it has some limitations:
- ❌ loses
undefined,NaN,Infinity,-0 - ❌ throws
TypeErrorserializing circular references - ❌ cannot handle
BigInt,Date,Error,Map,RegExp,Set,URL - ❌ doesn't support many other features...
This package is intended to offer something as great as JSON... trying to add something more.
- ✔ extends JSON
- ✔ safe parser: doesn't use
eval - ✔ JavaScript compatible: same result from
parseandeval - ✔ includes TypeScript types
- ✔ supports C style comments
- ✔ supports escaped new line in strings
- ✔ supports trailing commas
- ✔ supports circular and repeated references
- ✔ supports
undefined - ✔ supports
-0,NaNandInfinity - ✔ supports
BigInt - ✔ supports
Date - ✔ supports
Error(with exception) - ✔ supports
Map - ✔ supports
RegExp - ✔ supports
Set - ✔ supports
TypedArrays (butFloat16Array) - ✔ supports
URL
const set = new Set();
const arr = [set];
const obj = { arr, nan: NaN, set };
arr.push(arr, obj);
obj.obj = obj;
set.add(arr).add(obj);
const serialized = NJSON.stringify(obj);
const parsed = NJSON.parse(serialized);
console.log(parsed === parsed.obj); // true
console.log(parsed.arr === parsed.arr[1]); // true
console.log(isNaN(parsed.nan)); // true
console.log(parsed.set instanceof Set); // true
console.log(parsed === [...parsed.set][1]); // true
console.log(serialized);
// ((A,B,C)=>Object.assign(B,{"arr":Object.assign(A,{"0":C.add(A).add(B),"1":A,"2":B}),"set":C,"obj":B}))([],{"nan":NaN},new Set())| Feature | JSON | flatted | devalue | superjson | NJSON |
|---|---|---|---|---|---|
Safe: no eval use |
✔ | ✔ | ✔ | ✔ | ✔ |
eval compliant * |
✔ | ❌ | ❌ | ❌ | ✔ |
| Circular / repeated refs | ❌ | ✔ | ✔ | ✔ | ✔ |
Map / Set |
❌ | ❌ | ✔ | ✔ | ✔ |
BigInt |
❌ | ❌ | ✔ | ✔ | ✔ |
Date |
❌ | ❌ | ✔ | ✔ | ✔ |
RegExp |
❌ | ❌ | ✔ | ✔ | ✔ |
TypedArrays |
❌ | ❌ | ✔ | ❌ | ✔ |
Preserves undefined |
❌ | ❌ | ✔ | ✔ | ✔ |
* This is the main reason why
NJSONwas born. The string produced by any other option strictly requires its own parser to be converted back to the original value. As withJSON, the string produced byNJSONcan be cut / pasted in any JavaScript environment - without the need of any additional library - to reproduce the original value; a very powerful feature while debugging / developing.
This doesn't mean it's 100% compliant: due its higher number of supported features the result string of the
serialization through NJSON.stringify may differs from the result of the serialization through JSON.stringify.
On the other hand, the result of the deserialization of a valid JSON encoded string through NJSON.parse will
produce a value deep equal to the value produced by JSON.parse and the reviver function will be called the same
amount of times, with the same parameters and in the same order.
Note: the reviver function still not implements the newly added context argument.
Taken the result of a JSON.parse call (i.e. a value which contains only valid JSON values), if serialized through
JSON.stringify or NJSON.stringify produces two equal strings and the replacer function will be called the same
amount of times, with the same parameters and in the same order.
NJSON offers its own parser which means it doesn't use eval with its related security hole.
Even if the NJSON serialized string is JavaScript compliant, NJSON.parse is not able to parse any JavaScript
code, but only the subset produced by NJSON.stringify (otherwise it would have been another eval implementation).
NJSON do not supports some Objects by design; when one of them is encountered during the serialization process
NJSON tries to act as JSON does. Nonetheless they take part in the repeated references algorithm anyway. Follow
the details.
ArrayBuffers can't be manipulated by JavaScript design: they are serialized as empty objects as JSON does.
NJSON is designed to serialize / deserialize complex data to be shared between different systems, possibly written with other languages than JavaScript (once implementations in other languages will be written). Even if JavaScript can see a function as a piece of data, it is actually code, not data. More than this, for other languages, may be a complex problem to execute JavaScript functions.
Last but not least, allowing the deserialization of a function would open once again the security hole implied by the
use of eval, and one of the reasons why NJSON was born, is exactly to avoid that security hole.
A Symbol is something strictly bound to the JavaScript execution environment which instantiates it: sharing it
between distinct systems is something almost meaningless.
Note: except for Int8Array, Uint8Array and Uint8ClampedArray, TypedArrays are platform dependant: they are
supported (but Float16Array as it is not supported by Node.js), but trying to transfer one of them between
different architectures may be source of unexpected problems.
Error's are special objects. By specifications the properties cause, message, name and stack are not
enumerable, NJSON serializes them as any other property. This, plus the nature of the stack property, originates
the Error exception to the rule that an NJSON encoded string produces exactly the same value if parsed or
evaluated.
-
cause:- through
NJSON.parsethe result is a not enumerable property; - through
evalthe result may be an enumerable or a not enumerable property depending on the running JavaScript engine;
- through
-
stack:-
if absent:
- through
NJSON.parsethe result is a not enumerable property with value a pseudo-stack; - through
evalthe result is the standardstackproperty for the running JavaScript engine;
- through
-
if present:
- through
NJSON.parsethe result is a not enumerable property; - through
evalthe result may be an enumerable or a not enumerable property depending on the running JavaScript engine;
- through
-
The only option in my mind to avoid this exception is the use of Object.defineProperties, but it would increase both
the complexity of the parser and the size of the produced serialized string. Maybe in the future... configurable
through an option... if this can't be really tolerated.
With npm:
npm install --save next-jsonimport { NJSON } from "next-json";
const serialized = NJSON.stringify({ some: "value" });
const deserialized = NJSON.parse(serialized);import { NJSON, NjsonParseOptions, NjsonStringifyOptions } from "next-json";
const serialized = NJSON.stringify({ some: "value" });
const deserialized = NJSON.parse<{ some: string }>(serialized);import express from "express";
import { expressNJSON } from "next-json";
const app = express();
app.use(expressNJSON()); // install the polyfill
app.all("/mirror", (req, res) => res.njson(req.body)); // there is an 'n' more than usual
app.listen(3000);import { NJSON, fetchNJSON } from "next-json";
fetchNJSON(); // install the polyfill
const payload = { infinity: Infinity };
payload.circular = payload;
const response = await fetch("http://localhost:3000/mirror", {
body: NJSON.stringify(payload), // there is an 'N' more than usual
headers: { "Content-Type": "application/njson" }, // there is an 'n' more than usual
method: "POST"
});
const body = await response.njson(); // there is an 'n' more than usualHere payload deep equals payload.circular, which deep equals body, which deep equals body.circular, which deep
equals req.body in server side, which deep equals req.body.circular in server side! 🎉
The MIME type for NJSON format is: application/njson .
Just for compatibility with JSON.parse. Alias for:
NJSON.parse(text, { reviver });Converts a Next JavaScript Object Notation (NJSON) string into an object.
text: <string> The text to deserialize.options: <NjsonParseOptions> Deserialization options.- Returns: <unknown> The value result of
the deserialization of the NJSON encoded
text.
Just for compatibility with JSON.stringify. Alias for:
NJSON.stringify(value, { replacer, space });Converts a JavaScript value to a Next JavaScript Object Notation (NJSON) string.
value: <unknown> The value to serialize.options: <NjsonStringifyOptions> Serialization options.- Returns: <string> The
NJSON encoded serialized form of
value.
numberKey: <boolean> Alters the type of thekeyargument forreviver. Default:false.reviver: <Function> Alters the behavior of the deserialization process. Default:null.
If true, the reviver function, for Array elements, will be called with the key argument in a Number form.
As the
reviver
parameter of JSON.parse. See also replacer / reviver for NJSON specific details.
Note: the reviver function still not implements the newly added context argument.
date: <string> SpecifiesDates conversion method. Default:"time".numberKey: <boolean> Alters the type of thekeyargument forreplacer. Default:false.omitStack: <boolean> Specifies if to stringifystackforErrors. Default:false.replacer: <Function> | <Array> |nullAlters the behavior of the serialization process. Default:null.sortKeys: <boolean> Specifies whether to sortObjectkeys. Default:false.space: <number> | <string> |nullSpecifies the indentation. Default:null.stringLength: <number> |nullMakesStrings to be treated as references. Default:null.undef: <boolean> Specifies theundefinedbehavior. Default:true.
Specifies the method of Date objects used to serialize them. Follows the list of the allowed values and the relative
method used.
"iso":Date.toISOString()"string":Date.toString()"time":Date.getTime()- the default"utc":Date.toUTCString()
If true, the replacer function, for Array elements, will be called with the key argument in a Number form.
For default NJSON.stringify serializes the stack property for Errors. If set to true, the property is omitted
from the serialized representation.
As the
replacer
parameter of JSON.serialize. See also replacer / reviver for NJSON specific details.
For default NJSON stringifies (and replaces as well) Object keys in the order they appear in the Object itself.
If set to true, Object keys are sorted alphabetically before both the processes. This can be useful to compare two
references: using this option, the stringified representation of two deep equal references are two equal strings.
As the
space
parameter of JSON.serialize.
If specified, Strings which length is greater or equal to the specified value take part in the repeated references
algorithm.
For default NJSON.stringify serializes undefined values as well. If set to false, undefined values are
treated as JSON.stringify does.
An Express middleware which works as NJSON body parser and installs the
Express.Response.njson method.
options: <NjsonStringifyOptions> NJSON Express middleware options.- Returns: Express middleware The NJSON Express middleware.
parse: <NjsonParseOptions> Theoptionspassed toNJSON.parseby the middleware to parse the request body.stringify: <NjsonStringifyOptions> The defaultoptionspassed toNJSON.stringifybyExpress.Response.njsonto serialize the response body.
The options passed to NJSON.parse by the middleware to parse the request body.
The default options passed to NJSON.stringify by
Express.Response.njson to serialize the response.
Encodes the body in NJSON format and sends it; sets the proper Content-Type header as well. Installed by
expressNJSON.
body: <unknown> The body to be sent serialized.options: <NjsonStringifyOptions> Theoptionspassed toNJSON.stringifyto serialize the response body; overrides the defaultoptions.stringifypassed toexpressNJSON.
Installs the Response.njson method.
options: <NjsonParseOptions> The defaultoptionspassed toNJSON.parsebyResponse.njsonto parse the response body.
Parses the response body with NJSON.parse. Installed by
fetchNJSON.
options: <NjsonParseOptions> Theoptionspassed toNJSON.parseto parse the response body; overrides the defaultoptionspassed tofetchNJSON.- Returns: <unknown> The body parsed with
NJSON.parse.
Even if Date, RegExp, TypedArrays and URL are Objects, they are treated as native values i.e. replacer and
reviver will be never called with one of them as this context.
For Arrays the key argument is a positive integer, but in a String form for JSON compatibility. This can be
altered (i.e. in a Number form) through the numberKey option.
Map's keys can be Functions and Symbols; for Maps the key argument is a positive integer in a Number form
and the value argument is the entry in the form [mapKey, mapValue]. This gives a way to replace/revive keys
which can't be serialized. If replacer or reviver do not return a two elements array, the value is omitted.
For Sets the key argument is a positive integer and it is passed in a Number form.
Unlike JSON, NJSON does not call replacer and reviver for each element.
Regardless of whether they are omitted, serialized as native values or not, every Objects (but null), Functions
and Symbols take part in the repeated references algorithm; long Strings can take part as well (refer to
NjsonStringifyOptions.stringLength for details).
When a repeated reference is encountered, replacer and reviver are called against the reference, but it is not
called recursively against its properties. If a property of a repeated reference is changed, the same change has effect
in all other occurrences of the same reference.
Circular references are simply special cases of repeated references.
v0.5.0 changes the stringification of Arrays from the use of push to the use Object.assign with some
advantages:
-
enables the support of sparse arrays, next item in the TODO list;
-
enables the use of body-less functions rather than functions which makes the parser thinner;
and one disadvantage:
- breaks the compatibility with previous versions stringification.
To make the change as smooth as possible, v0.5.0 still supports the deserialization of previous versions
stringification: support for parsing push will be removed in some later version TBD.
If
NJSONis used to produce volatile values throughstringifythat are immediately parsed and discarded, no actions are required.If some values stringified using
NJSON.version<v0.5.0are stored (in some DBs or in some files), it is recommended to convert them serialization to the newer format (just parse and re-stringify them usingv0.5.0<=NJSON.version<TBD).
Requires Node.js v14.
Exception: fetchNJSON requires Node.js v18.
The package is tested under all Node.js versions currently supported accordingly to Node.js Release.
TypeScript types are distributed with the package itself.
Do not hesitate to report any bug or inconsistency @github.
If you find useful this package, please consider the opportunity to donate on one of following cryptos:
ADA: DdzFFzCqrhsxfKAujiyG5cv3Bz7wt5uahr9k3hEa8M6LSYQMu9bqc25mG72CBZS3vJEWtWj9PKDUVtfBFcq5hAtDYsZxfG55sCcBeHM9
BTC: 3BqXRqgCU2CWEoZUgrjU3b6VTR26Hee5gq
ETH: 0x8039fD67b895fAA1F3e0cF539b8F0290EDe1C042
Other projects which aim to solve similar problems: