Subscribe to my email list now at http://jauyeung.net/subscribe/
Follow me on Twitter at https://twitter.com/AuMayeung
Many more articles at https://medium.com/@hohanga
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.
Top comments (12)
Another hidden ability of JavaScript JSON is
Date.prototype.toJSON
, which will customizeJSON.stringify
behavior of the object.Thanks for finding that.
That's a method that most people don't think of using.
Just so you know, including multiple return paths for functions is an anti-pattern/code-smell. It is a best practice for a function to only have a single return path if that is possible. So, for example, in your third to last snippet, you could have written the
replacer
function like this instead and have what it returns be the same (I also fixed your NaN check to use Number.isNaN() because using equality to check against NaN will not work as expected):Absolutely wrong! In every case is switch more readable than joined or/and superlongline. More easy to update. And you will not make error in it so easily like in bunch of writeonly code you just wrote.
I think ternary operator shouldn't be nested. It's harder to read than if or switch like you said.
value === NaN
is always false. For exampleNaN === NaN
. Use isNaN().Thanks. That's a good catch.
You should actually be using Number.isNan() over the global isNaN function which will give somewhat unexpected results in comparison to the newer method on the Number object. You should also use !Number.isFinite() instead of checking equality with Infinity.
Thanks. That's also a good tip.
I think equality is still good as long as we use triple equals.
Yes, you can still write it that way.
Also if the object or class that you're converting has a toJSON function, it will use that to determine the output - can be very handy :)
developer.mozilla.org/en-US/docs/W...
Yea. Then we don't have to passing a mapping function every time we want to stringify. I think URL and URLSearchParams objects have this method.