Categories
JavaScript

A Guide to JSON and How It’s Handled in JavaScript

Spread the love

JSON stands for JavaScript Object Notation. It is a format for serializing data, which means that it can be used to transmit and receive data between different sources. In JavaScript, there’s a JSON utility object that provides methods to convert JavaScript objects to JSON strings and vice versa. The JSON utility object can’t be constructed or called — there are only 2 static methods which are stringify and parse to convert between JavaScript objects and JSON strings.

Properties of JSON

JSON is a syntax for serializing objects, arrays, numbers, booleans, and null. It is based on the JavaScript object syntax, but they are not the same thing. Not all JavaScript object properties can be converted to valid JSON, and JSON strings must be correctly formatted to be converted into a JavaScript object.

For objects and arrays, JSON property names must be in double-quoted strings, and trailing commas for objects are prohibited. Numbers cannot have leading zeroes, and a decimal point must be followed by at least one digit. NaN and Infinity aren’t supported, and JSON strings can’t have undefined or comments. In addition, JSON can not contain functions.

Any JSON text must contain valid JavaScript expressions. In some browser engines, the U+2028 line separator and U+2029 paragraph separator are allowed in string literals and property keys in JSON, but when using them in JavaScript code will result in SyntaxError. Those 2 characters can be parsed with JSON.parse into valid JavaScript strings, but fails when passed into eval.

Insignificant whitespace may be included anywhere except within JSONNumber or JSONString. Numbers can’t have whitespace inside and strings would be interpreted as whitespace in the string or cause an error. The tab character (U+0009), carriage return (U+000D), line feed (U+000A), and space (U+0020) characters are the only valid whitespace characters in JSON.

Basic Usage of the JSON Object

There are 2 methods on the JSON utility object. There is the stringify method for converting a JavaScript object to a JSON string and the parse method for converting a JSON string to a JavaScript object.

The parse method parses a string as JSON with a function as a second argument to optionally transform JSON entities to the JavaScript entity that you specified and return the resulting JavaScript object. If the string has entities that aren’t allowed in the JSON syntax, then a SyntaxError would be raised. Also, tailing commas aren’t allowed in the JSON string that is passed into JSON.parse. For example, we can use it as in the following code:

JSON.parse('{}'); // {}       
JSON.parse('false'); // false        
JSON.parse('"abc"'); // 'abc'         
JSON.parse('[1, 5, "abc"]');  // [1, 5, 'abc']  
JSON.parse('null'); // null

The first line would return an empty object. The second would return false. The third line would return 'abc'. The fourth line would return [1, 5, "abc"]. The fifth line would return null. It returns what we expect since every line we pass in is valid JSON.

Customize the Behavior of Stringify and Parse

Optionally, we can pass in a function as the second argument to convert values to whatever we want. The function we pass in will take the key as the first parameter and the value as the second and returns the value after manipulation is done. For example, we can write:

JSON.parse('{"a:": 1}', (key, value) =>  
  typeof value === 'number'  
    ? value * 10  
    : value       
);

Then we get {a: 10} returned. The function returns the original value multiplied by 10 if the value’s type is a number.

The JSON.stringify method can take a function as the second parameter that maps entities in the JavaScript object to something else in JSON. By default, all instances of undefined and unsupported native data like functions are removed. For example, if we write the following code:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc'  
}  
const jsonString = JSON.stringify(obj);  
console.log(jsonString);

Then we see that fn1 is removed from the JSON string after running JSON.stringify since functions aren’t supported in JSON syntax. For undefined, we can see from the following code that undefined properties will be removed.

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined  
}  
const jsonString = JSON.stringify(obj);  
console.log(jsonString);

undefinedProp is not in the JSON string logged because it has been removed by JSON.strinfiy.

Also, NaN and Infinity all become null after converting to a JSON string:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity  
}  
const jsonString = JSON.stringify(obj);  
console.log(jsonString);

We see that:

'{“foo”:1,”bar”:2,”abc”:”abc”,”nullProp”:null,”notNum”:null,”infinity”:null}'

NaN and Infinity have both become null instead of the original values.

For unsupported values, we can map them to supported values with the replacer function in the second argument which we can optionally pass in. The replace function takes the key of a property as the first parameter and the value as the second parameter. For example, one way to keep NaN, Infinity, or functions is to map them to a string like in the following code:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity  
}

const replacer = (key, value) => {  
  if (value instanceof Function) {  
    return value.toString();  
  } 
  else if (value === NaN) {  
    return 'NaN';  
  } 
  else if (value === Infinity) {  
    return 'Infinity';  
  } 
  else if (typeof value === 'undefined') {  
    return 'undefined';  
  } 
  else {  
    return value; // no change  
  }  
}

const jsonString = JSON.stringify(obj, replacer, 2);  
console.log(jsonString);

After running console.log on jsonString in the last line, we see that we have:

{  
  "fn1": "fn1() {}",  
  "foo": 1,  
  "bar": 2,  
  "abc": "abc",  
  "nullProp": null,  
  "undefinedProp": "undefined",  
  "notNum": null,  
  "infinity": "Infinity"  
}

What the replace function did was add additional parsing using the key and the value from the object being converted with JSON.stringify. It checks that if the value is a function, then we convert it to a string and return it. Likewise, with NaN, Infinity, and undefined, we did the same thing. Otherwise, we return the value as-is.

The third parameter of the JSON.stringfy function takes in a number to set the number of whitespaces to be inserted into the output of the JSON to make the output more readable. The third parameter can also take any string that will be inserted instead of whitespaces. Note that if we put a string as the third parameter that contains something other than white space(s), we may create a “JSON” a string that is not valid JSON.

For example, if we write:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity  
}
const replacer = (key, value) => {  
  if (value instanceof Function) {  
    return value.toString();  
  } 
  else if (value === NaN) {  
    return 'NaN';  
  } 
  else if (value === Infinity) {  
    return 'Infinity';  
  } 
  else if (typeof value === 'undefined') {  
    return 'undefined';  
  } 
  else {  
    return value; // no change  
  }  
}
const jsonString = JSON.stringify(obj, replacer, 'abc');  
console.log(jsonString);

Then console.log will be:

{  
abc"fn1": "fn1() {}",  
abc"foo": 1,  
abc"bar": 2,  
abc"abc": "abc",  
abc"nullProp": null,  
abc"undefinedProp": "undefined",  
abc"notNum": null,  
abc"infinity": "Infinity"  
}

Which is obviously not valid JSON. JSON.stringify will throw a “cyclic object value” TypeError. Also, if an object has BigInt values, then the conversion will fail with a “BigInt value can’t be serialized in JSON” TypeError.

Also, note that Symbols are automatically discarded with JSON.stringify if they are used as a key in an object. So if we have:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity,  
  [Symbol('foo')]: 'foo'  
}

const replacer = (key, value) => {
  if (value instanceof Function) {  
    return value.toString();  
  } 
  else if (value === NaN) {  
    return 'NaN';  
  } 
  else if (value === Infinity) {  
    return 'Infinity';  
  } 
  else if (typeof value === 'undefined') {  
    return 'undefined';  
  } 
  else {  
    return value; // no change  
  }  
}

const jsonString = JSON.stringify(obj, replacer, 2);  
console.log(jsonString);

We get back:

{  
  "fn1": "fn1() {}",  
  "foo": 1,  
  "bar": 2,  
  "abc": "abc",  
  "nullProp": null,  
  "undefinedProp": "undefined",  
  "notNum": null,  
  "infinity": "Infinity"  
}

Date objects are converted to strings by using the same string as what date.toISOString() will return. For example, if we put:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity,  
  [Symbol('foo')]: 'foo',  
  date: new Date(2019, 1, 1)  
}  
const replacer = (key, value) => {  
  if (value instanceof Function) {  
    return value.toString();  
  } 
  else if (value === NaN) {  
    return 'NaN';  
  } 
  else if (value === Infinity) {  
    return 'Infinity';  
  } 
  else if (typeof value === 'undefined') {  
    return 'undefined';  
  } 
  else {  
    return value; // no change  
  }  
}  
const jsonString = JSON.stringify(obj, replacer, 2);  
console.log(jsonString);

We get:

{  
  "fn1": "fn1() {}",  
  "foo": 1,  
  "bar": 2,  
  "abc": "abc",  
  "nullProp": null,  
  "undefinedProp": "undefined",  
  "notNum": null,  
  "infinity": "Infinity",  
  "date": "2019-02-01T08:00:00.000Z"  
}

As we can see, the value of the date property is now a string after converting to JSON.

Deep Copy Objects

We can also use JSON.stringify with JSON.parse to make a deep copy of JavaScript objects. For example, to do a deep copy of a object without a library, you can JSON.stringify then JSON.parse:

const a = { foo: {bar: 1, {baz: 2}}  
const b = JSON.parse(JSON.stringfy(a)) // get a clone of a which you can change with out modifying a itself

This does a deep copy of an object, which means all levels of an object are cloned instead of referencing the original object. This works because JSON.stringfy converted the object to a string which are immutable, and a copy of it is returned when JSON.parse parses the string which returns a new object that doesn’t reference the original object.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *