41. Creating and parsing JSON (JSON)
JSON (“JavaScript Object Notation”) is a storage format that uses text to encode data. Its syntax is a subset of JavaScript expressions. As an example, consider the following data, stored as text in a file jane.json
:
JavaScript has the global namespace object JSON
provides methods for creating and parsing JSON.
41.1. The discovery and standardization of JSON
A specification for JSON was published by Douglas Crockford in 2001, at json.org
. He explains:
I discovered JSON. I do not claim to have invented JSON, because it already existed in nature. What I did was I found it, I named it, I described how it was useful. I don’t claim to be the first person to have discovered it; I know that there are other people who discovered it at least a year before I did. The earliest occurrence I’ve found was, there was someone at Netscape who was using JavaScript array literals for doing data communication as early as 1996, which was at least five years before I stumbled onto the idea.
Later, JSON was standardized as ECMA-404:
- 1st edition: October 2013
- 2nd edition: December 2017
41.1.1. JSON’s grammar is frozen
Quoting the ECMA-404 standard:
Because it is so simple, it is not expected that the JSON grammar will ever change. This gives JSON, as a foundational notation, tremendous stability.
Therefore, JSON will never get improvements such as optional trailing commas, comments or unquoted keys – independently of whether or not they are considered desirable. However, that still leaves room for creating supersets of JSON that compile to plain JSON.
41.2. JSON syntax
JSON consists of the following parts of JavaScript:
- Compound:
- Object literals:
- Keys are double-quoted strings.
- Values are JSON values.
- No trailing commas are allowed.
- Array literals:
- Elements are JSON values.
- No holes or trailing commas are allowed.
- Object literals:
- Atomic:
null
(but notundefined
)- Booleans
- Numbers (excluding
NaN
,+Infinity
,-Infinity
) - Strings (must be double-quoted)
As a consequence, you can’t (directly) represent cyclic structures in JSON.
41.3. Using the JSON API
The global namespace object JSON
contains methods for working with JSON data.
41.3.1. JSON.stringify(value, replacer?, space?)
.stringify()
converts a JavaScript value
to a JSON string.
41.3.1.1. Result: a single line of text
If you only provide the first argument, .stringify()
returns a single line of text:
41.3.1.2. Result: a tree of indented lines
If you provide a non-negative integer for space
(we are ignoring replacer
here, which is explained later), then .stringify()
returns one or more lines and indents by space
spaces per level of nesting:
41.3.1.3. Details on how JavaScript values are stringified
Supported primitive values are stringified as expected:
Non-finite numbers (incl. NaN
) are stringified as 'null'
:
Unsupported primitive values are stringified as undefined
:
Functions are stringified as undefined
:
In an Array, elements that would be stringified as undefined
, are stringified as 'null'
:
In an object (that is neither an Array nor a function), properties, whose values would be stringified as undefined
, are skipped:
If an object (which may be an Array or a function) has a method .toJSON()
, then the result of that method is stringified, instead of the object. For example, Dates have a method .toJSON()
that returns strings.
For more details on stringification, consult the ECMAScript specification.
41.3.2. JSON.parse(text, reviver?)
.parse()
converts a JSON text
to a JavaScript value:
The parameter reviver
is explained later.
41.3.3. Example: converting to and from JSON
The following class demonstrates one technique for implementing the conversion from and to JSON:
class Point {
static fromJson(jsonObj) {
return new Point(jsonObj.x, jsonObj.y);
}
constructor(x, y) {
this.coord = [x, y];
}
toJSON() {
const [x, y] = this.coord;
return {x, y};
}
}
assert.equal(
JSON.stringify(new Point(3, 5)),
'{"x":3,"y":5}');
assert.deepEqual(
Point.fromJson(JSON.parse('{"x":3,"y":5}')),
new Point(3, 5));
The previously mentioned method .toJSON()
is used when stringifying instances of Point
.
41.4. Configuring what is stringified or parsed (advanced)
What is stringified or parsed, can be configured as follows:
.stringify()
has the optional parameterreplacer
that contains either:- An Array with names of properties. When stringifying an object (that may be nested) only those properties will be considered, all others will be ignored.
- A value visitor – a function that can transform JavaScript values before they are stringified.
.parse()
has the optional parameterreviver
– a value visitor that can transform the parsed JSON data before it is returned.
41.4.1. .stringfy(): specifying the only properties that objects should have
If the second parameter of .stringify()
is an Array, then only object properties, whose names are mentioned in the Array, are included in the result:
41.4.2. .stringify() and .parse(): value visitors
What I call a value visitor is a function that transforms JavaScript values (compound or atomic):
JSON.stringify()
calls the value visitor in its parameterreplacer
before it stringfies the JavaScript value it received.JSON.parse()
calls the value visitor in its parameterreviver
after it parsed the JSON data.
A JavaScript value is transformed as follows: The value is either atomic or compound and contains more values (nested in Arrays and objects). The atomic value or the nested values are fed to the value vistor, one at a time. Depending on what the visitor returns, the current value is removed, changed or preserved.
A value visitor has the following type signature:
The parameters are:
value
: The current value.this
: Parent of current value. The parent of the root valuer
is{'': r}
.key
: Key or index of the current value inside its parent. The empty string is used for the root value.
The value visitor can return:value
: means there won’t be any change.- A different value
x
: leads tovalue
being replaced withx
. undefined
: leads tovalue
being removed.
41.4.3. Example: visiting values
The following code demonstrates in which order a value visitor sees values.
const log = [];
function valueVisitor(key, value) {
log.push({key, value, this: this});
return value; // no change
}
const root = {
a: 1,
b: {
c: 2,
d: 3,
}
};
JSON.stringify(root, valueVisitor);
assert.deepEqual(log, [
{ key: '', value: root, this: { '': root } },
{ key: 'a', value: 1, this: root },
{ key: 'b', value: root.b, this: root },
{ key: 'c', value: 2, this: root.b },
{ key: 'd', value: 3, this: root.b },
]);
As you can see, .stringify()
visits values top-down (root first, leaves last). In contrast, .parse()
visits values bottom-up (leaves first, root last).
41.4.4. Example: stringifying unsupported values
.stringify()
has no special support for regular expression objects – it stringifies them as if they were plain objects:
We can fix that via a replacer:
function replacer(key, value) {
if (value instanceof RegExp) {
return {
__type__: 'RegExp',
source: value.source,
flags: value.flags,
};
} else {
return value; // no change
}
}
assert.equal(
JSON.stringify(obj, replacer, 2),
`{
"name": "abc",
"regex": {
"__type__": "RegExp",
"source": "abc",
"flags": "iu"
}
}`);
41.4.5. Example: parsing unsupported values
To .parse()
the result from the previous section, we need a reviver:
function reviver(key, value) {
// Very simple check
if (value && value.__type__ === 'RegExp') {
return new RegExp(value.source, value.flags);
} else {
return value;
}
}
const str = `{
"name": "abc",
"regex": {
"__type__": "RegExp",
"source": "abc",
"flags": "iu"
}
}`;
assert.deepEqual(
JSON.parse(str, reviver),
{
name: 'abc',
regex: /abc/ui,
});
41.5. FAQ
41.5.1. Why doesn’t JSON support comments?
Douglas Crockford explains why in a Google+ post from 1 May 2012:
I removed comments from JSON because I saw people were using them to hold parsing directives, a practice which would have destroyed interoperability. I know that the lack of comments makes some people sad, but it shouldn’t.Suppose you are using JSON to keep configuration files, which you would like to annotate. Go ahead and insert all the comments you like. Then pipe it through JSMin [a minifier for JavaScript] before handing it to your JSON parser.
41.6. Quick reference: JSON
Signature of value visitors:
JSON
:
.stringify(value: any, replacer?: ValueVisitor, space?: string | number): string
[ES5]
Convert value
to a JSON string. The parameter replacer
is explained earlier in this chapter. The parameter space
works as follows:
- If
space
is omitted,.stringify()
returns a single line of text.
- If
space
is a number,.stringify()
returns one or more lines and indents them byspace
spaces per level of nesting.
- If
space
is a string, it is used to indent.
.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string
[ES5]
If replacer
is an Array, then the result only includes object properties whose names are mentioned in the Array.
.parse(text: string, reviver?: ValueVisitor): any
[ES5]
Parse the JSON inside text
and return a JavaScript value. The parameter reviver
is explained earlier in this chapter.