avatarJennifer Fu

Summary

This content provides an in-depth guide to JavaScript Object Notation (JSON) and its more flexible counterpart, JSON5, and discusses how to handle circular references in JSON and JSON5.

Abstract

In this content, the reader is introduced to JavaScript Object Notation (JSON), a lightweight data-interchange format that is easy to read and write for humans and machines alike. JSON is based on a subset of the JavaScript Programming Language Standard and has been an international standard since October 2013. The limitations of JSON, such as its limited support for data types and lack of namespace, comment, or attribute support, make it a simple and fast format to transmit and parse. However, these limitations can sometimes cause issues when working with complex configurations.

The content also introduces JSON5, a superset of JSON that alleviates some of the limitations of JSON by expanding its syntax to include productions from ECMAScript 5.1. JSON5 supports object keys that are ECMAScript 5.1 identifier names, single-quoted strings, string escapes, hexadecimal numbers, numbers with a leading or trailing decimal point, and the values Infinity, -Infinity, and NaN. JSON5 also supports single and multi-line comments and additional white space characters.

The content then discusses how to handle circular references in JSON and JSON5. Circular references occur when an object references itself, either directly or indirectly, and can cause issues when trying to convert the object to a string or serialize it using JSON or JSON5. The content provides two ways to resolve this issue: using the replacer function in JSON.stringify and using third-party packages such as json-stringify-safe.

Bullet points

  • JSON is a lightweight data-interchange format that is easy to read and write for humans and machines alike.
  • JSON is based on a subset of the JavaScript Programming Language Standard and has been an international standard since October 2013.
  • JSON has limited support for data types and lacks namespace, comment, or attribute support.
  • JSON5 is a superset of JSON that alleviates some of the limitations of JSON by expanding its syntax to include productions from ECMAScript 5.1.
  • Circular references can cause issues when trying to convert an object to a string or serialize it using JSON or JSON5.
  • Circular references can be resolved using the replacer function in JSON.stringify or using third-party packages such as json-stringify-safe.

Exploring JSON, JSON5, and Circular References

An in-depth guide on JavaScript Object Notation (JSON)

Photo by Erlend Ekseth on Unsplash

Object toString Problem

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).

What is 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 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 to 0.
  • The second time is for the key, b, and 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, 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": 1\n}' means the following structure:

{
  "a": 1
}

What is JSON5?

JSON is cool, and JSON5 is even more cool!

The JSON5 Data Interchange Format (JSON5) is a superset of JSON that aims to alleviate some of the limitations of JSON by expanding its syntax to include some productions from ECMAScript 5.1.

Objects

  • Object keys may be an ECMAScript 5.1 identifier name.
  • Objects may have a single trailing comma.

Arrays

  • Arrays may have a single trailing comma.

Strings

  • Strings may be single quoted.
  • Strings may span multiple lines by escaping new line characters.
  • Strings may include character escapes.

Numbers

  • Numbers may be hexadecimal.
  • Numbers may have a leading or trailing decimal point.
  • Numbers may be Infinity, -Infinity, and NaN.
  • Numbers may begin with an explicit plus sign.

Comments

  • Single and multi-line comments are allowed.

White Space

  • Additional white space characters are allowed.

JSON5’s static APIs, parse and stringify, are the same as JSON’s.

In order to use JSON5, we need to install the package, json5, which has 50 million weekly downloads.

npm i json5

json5 becomes part of dependencies in package.json:

Then, we can import parse and stringify from json5. Some of our failed cases work with JSON5.

How To Convert Circular Structure

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 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:

Conclusion

JSON is a lightweight data-interchange format. It handles object serialization more gracefully than object’s toString().

We have looked in details on how JSON and JSON5 work, and provided multiple ways to resolve the circular reference issue.

Thanks for reading. I hope this was helpful. If you are interested, check out my other Medium articles.

Programming
Json
JavaScript
Web Development
Software Development
Recommended from ReadMedium