An in-depth guide on JavaScript Object Notation (JSON)
In JavaScript and TypeScript, an object is a collection of properties. For example:
const value = {a: 1, b: 2};
The value can be logged:
console.log(value); // {a: 1, b: 2}
How about a composed message?
console.log(`My value is ${value}`); // My value is [object Object]
Ops!
What is [object Object]
? It is the object’s toString()
value.
console.log
message can be fixed by suppling objects as independent parameters.
console.log('My value is', value); // My value is {a: 1, b: 2}
However, there are other cases that serialize objects using toString()
. For example, a JSX element:
<div>{value}</div>
In many situations, JSON.stringify()
is the rescuer.
We are going to take a close look at JavaScript Object Notation (JSON).
JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to read and write, and it is also easy for machines to parse and generate. It is based on a subset of the JavaScript Programming Language Standard ECMA-262 3rd Edition — December 1999.
JSON was specified first by Douglas Crockford in March 2001. It is a syntax for serializing objects, arrays, numbers, strings, booleans, and null
. It became an ECMA international standard in October 2013.
JSON has two main functionalities:
- It is a data format transferred between the client and the server.
- It is used to define configurations.
JSON syntax is simple, with limited support for data types, which include object
, array
, number
, string
, boolean
and null
. However, functions, NaN
, Infinity
, undefined
and Symbol
are not valid JSON values. JSON has no namespace, comment, or attribute support. It may not support complex configurations. These limitations make JSON simple, and hence it is transmitted and parsed fast.
JSON has two static methods, JSON.parse()
and JSON.stringify()
.
JSON.parse()
JSON.parse(text)
parses a JSON string to construct a JavaScript value or object. For objects, JSON’s property names must be double-quoted strings, and trailing commas are forbidden. For primitive types, JSON.parse()
returns primitive values. For numbers, leading zeros are prohibited, and a decimal point must be followed by at least one digit. Any of the violations of the JSON syntax will throw SyntaxError
.
Here are examples on how JSON.parse()
constructs JavaScript values or objects:
JSON.parse(text[, reviver])
has an optional reviver
which can alter the return value.
The reviver
(lines 2–12) is invoked 3 times.
- The first time is for the key,
a
and at line 4, the value (1
) is decreased to0
. - The second time is for the key,
b
and at line 8, the value (2
) is increased to3
. - The third time is for the key,
""
and the value is the current object ({"a":0,"b":3}
). At line 11, the return value is transformed to a string,key is "", and value is {"a":0,"b":3}
.
Line 15 logs key is "", and value is {"a":0,"b":3}
.
JSON.stringify()
JSON.stringify(value)
returns a JSON string corresponding to the specified value
. boolean
, number
and string
are converted to the corresponding primitive values. Functions undefined
and Symbol
are not valid JSON values, which are omitted in an object, or changed to null
in an array. NaN
, Infinity
and null
are changed to null
. If the value has a toJSON()
method, the data serialization calls this method. Date
implements the toJSON()
function by returning the string, date.toISOString()
. JSON cannot serialize BigInt
values or non-enumerable properties.
Here are examples on how JSON.stringify()
composes JSON strings:
JSON.stringify(value[, replacer])
has an optional replacer
which can alter the return value.
We write a replacer
that is similar to the reviver
in JSON.parse()
. Guess what will be logged?
The replacer
is defined at lines 2–12, and it outputs "key is "", and value is {"a":1,"b":2}"
.
A replacer
is opposite to a reviver
. The first invocation has the key, ""
and value, {"a":0,"b":3}
. At line 11, it returns a string value, "key is "", and value is {"a":1,"b":2}"
. Since it is a string that has no property for the next iteration, the replacer
exits. Line 15 logs "key is "", and value is {"a":1,"b":2}"
.
If we change line 11 to return {a: 5};
the next invocation will be on the property, a
. Since there is no other property, the replacer
exits. Line 15 logs {"a":4}
.
What if we change line 11 to return {c: 5};
? Then, the next invocation will be on the property, c
. Loop through the property, c
again, and it returns {c: 5}
. It becomes an infinite loop, and throws an error: Uncaught RangeError: Maximum call stack size exceeded
.
We should be very careful to write a replacer
. The first invocation for the empty key should simply return the original value. The following is a typical JSON.stringify
with a replacer
:
Line 13 logs {"a":0,"b":3}
.
JSON.stringify(value[, replacer[, space]])
has a second optional parameter, space
which is a string
or number
that inserts white spaces into the output JSON string for readability.
n
is the newline character, and n
is the tab character. '{n "a": 1n}'
means the following structure:
{
"a": 1
}
JSON/JSON5 is more powerful than the simple toString()
. JSON.stringify()
resolves the object serialization issue of [object Object]
. The original problem is resolved.
However, we get a new problem with JSON.stringify()
.
Have you ever encountered the error: Uncaught TypeError: Converting circular structure to JSON
?
As a JavaScript/TypeScript developer, you probably have encountered this error many times. Here is an example:
It is bad coding to create circular references. But, we may not have a choice if the bad JSON structures come from the backend or third-party packages.
How do we deal with circular references?
There are a couple of ways to resolve the issue:
JSON.stringify
‘sreplacer
- Third-party packages
JSON.stringify’s replacer
For circular references, JSON or JSON5 stringify
‘s replacer
can be the rescuer. Here is the example code on MDN website.
Lines 3–14 define a function, getCircularReplacer
. It returns a function that creates a closure of seen
which is a WeakSet
that stores weakly held objects in a collection. For each key, it verifies whether the value is already in seen
(line 7). If yes, it is a circular reference, the key/value pair is ignored (line 8). Otherwise, the value is added to seen
(line 10) and returned (line 12).
Line 15 calls getCircularReplacer
to return the replacer
function, and JSON.stringify()
outputs {"a":1,"b":2}
with the circular property removed.
Third-party packages
There are a number of third-party packages that resolve the circular reference issue. json-stringify-safe
is a popular one, which has 17 million weekly downloads. It is a package that works similar to JSON.stringify
, but does not throw on circular references. It can be set up with the following command:
npm i json-stringify-safe
json-stringify-safe
becomes part of dependencies
in package.json
:
The package has one method, stringify
. The method has four parameters, stringify(obj, serializer, indent, decycler)
. The first three parameters are the same as JSON.stringify
‘s. By default, stringify
shows the circular reference as a string, '[Circular]'
. decycler
can customize how it is displayed.
Line 5 shows the default stringify
result for the JSON structure with a circular reference.
Line 6 customizes the result to remove the circular referenced property.
Here is the code on how json-stringify-safe
defines stringify
: