Exploring JSON, JSON5, and Circular References | by Jennifer Fu | Mar, 2022

Photo by Erlend Ekseth on Unsplash

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, booleanand null. However, functions, NaN, Infinity, undefinedand 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 reviverwhich can alter the return value.

The reviver (lines 2–12) is invoked 3 times.

  • The first time is for the key, aand at line 4, the value (1) is decreased to 0.
  • The second time is for the key, band at line 8, the value (2) is increased to 3.
  • 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, numberand string are converted to the corresponding primitive values. Functions undefinedand Symbol are not valid JSON values, which are omitted in an object, or changed to null in an array. NaN, Infinityand 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 replacerwhich 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, cagain, 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, spacewhich 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‘s replacer
  • 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 seenwhich 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:

Leave a Comment