JSON - JavaScript in Depth - Speaking JavaScript (2014)

Speaking JavaScript (2014)

Part III. JavaScript in Depth

Chapter 22. JSON

JSON (JavaScript Object Notation) is a plain-text format for data storage. It has become quite popular as a data interchange format for web services, for configuration files, and more. ECMAScript 5 has an API for converting from a string in JSON format to a JavaScript value (parsing) and vice versa (stringifying).

Background

This section explains what JSON is and how it was created.

Data Format

JSON stores data as plain text. Its grammar is a subset of the grammar of JavaScript expressions. For example:

{

"first": "Jane",

"last": "Porter",

"married": true,

"born": 1890,

"friends": [ "Tarzan", "Cheeta" ]

}

JSON uses the following constructs from JavaScript expressions:

Compound

Objects of JSON data and arrays of JSON data

Atomic

Strings, numbers, booleans, and null

It adheres to these rules:

§ Strings must always be double-quoted; string literals such as 'mystr' are illegal.

§ Property keys must be double-quoted.

History

Douglas Crockford discovered JSON in 2001. He gave it a name and put up a specification at http://json.org:

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.

Initially, Crockford wanted JSON to have the name JavaScript Markup Language, but the acronym JSML was already taken by the JSpeech Markup Language.

The JSON specification has been translated to many human languages, and there are now libraries for many programming languages that support parsing and generating JSON.

Grammar

Douglas Crockford created a JSON business card with a logo on the front (see Figure 22-1) and the full grammar on the back (see Figure 22-2). That makes it visually obvious how positively simple JSON is.

The front side of the JSON business card shows a logo (source: Eric Miraglia).

Figure 22-1. The front side of the JSON business card shows a logo (source: Eric Miraglia).

The back side of the JSON business card contains the complete grammar (source: Eric Miraglia).

Figure 22-2. The back side of the JSON business card contains the complete grammar (source: Eric Miraglia).

The grammar can be transcribed as follows:

object

{ }

{ members }

members

pair

pair , members

pair

string : value

array

[ ]

[ elements ]

elements

value

value , elements

value

string

number

object

array

true

false

null

string

""

" chars "

chars

char

char chars

char

any-Unicode-character-except-"-or-\-or-control-character

\" \\ \/ \b \f \n \r \t

\u four-hex-digits

number

int

int frac

int exp

int frac exp

int

digit

digit1-9 digits

- digit

- digit1-9 digits

frac

. digits

exp

e digits

digits

digit

digit digits

e

e e+ e-

E E+ E-

The global variable JSON serves as a namespace for functions that produce and parse strings with JSON data.

JSON.stringify(value, replacer?, space?)

JSON.stringify(value, replacer?, space?) translates the JavaScript value value to a string in JSON format. It has two optional arguments.

The optional parameter replacer is used to change the value before stringifying it. It can be:

§ A node visitor (see Transforming Data via Node Visitors) that transforms the tree of values before it is stringified. For example:

§ function replacer(key, value) {

§ if (typeof value === 'number') {

§ value = 2 * value;

§ }

§ return value;

}

Using the replacer:

> JSON.stringify({ a: 5, b: [ 2, 8 ] }, replacer)

'{"a":10,"b":[4,16]}'

§ A whitelist of property keys that hides all properties (of nonarray objects) whose keys are not in the list. For example:

§ > JSON.stringify({foo: 1, bar: {foo: 1, bar: 1}}, ['bar'])

'{"bar":{"bar":1}}'

The whitelist has no effect on arrays:

> JSON.stringify(['a', 'b'], ['0'])

'["a","b"]'

The optional parameter space influences the formatting of the output. Without this parameter, the result of stringify is a single line of text:

> console.log(JSON.stringify({a: 0, b: ['\n']}))

{"a":0,"b":["\n"]}

With it, newlines are inserted and each level of nesting via arrays and objects increases indentation. There are two ways to specify how to indent:

A number

Multiply the number by the level of indentation and indent the line by as many spaces. Numbers smaller than 0 are interpreted as 0; numbers larger than 10 are interpreted as 10:

> console.log(JSON.stringify({a: 0, b: ['\n']}, null, 2))

{

"a": 0,

"b": [

"\n"

]

}

A string

To indent, repeat the given string once for each level of indentation. Only the first 10 characters of the string are used:

> console.log(JSON.stringify({a: 0, b: ['\n']}, null, '|--'))

{

|--"a": 0,

|--"b": [

|--|--"\n"

|--]

}

Therefore, the following invocation of JSON.stringify() prints an object as a nicely formatted tree:

JSON.stringify(data, null, 4)

Data Ignored by JSON.stringify()

In objects, JSON.stringify() only considers enumerable own properties (see Property Attributes and Property Descriptors). The following example demonstrates the nonenumerable own property obj.foo being ignored:

> var obj = Object.defineProperty({}, 'foo', { enumerable: false, value: 7 });

> Object.getOwnPropertyNames(obj)

[ 'foo' ]

> obj.foo

7

> JSON.stringify(obj)

'{}'

How JSON.stringify() handles values that are not supported by JSON (such as functions and undefined) depends on where it encounters them. An unsupported value itself leads to stringify() returning undefined instead of a string:

> JSON.stringify(function () {})

undefined

Properties whose values are unsupported are simply ignored:

> JSON.stringify({ foo: function () {} })

'{}'

Unsupported values in arrays are stringified as nulls:

> JSON.stringify([ function () {} ])

'[null]'

The toJSON() Method

If JSON.stringify() encounters an object that has a toJSON method, it uses that method to obtain a value to be stringified. For example:

> JSON.stringify({ toJSON: function () { return 'Cool' } })

'"Cool"'

Dates already have a toJSON method that produces an ISO 8601 date string:

> JSON.stringify(new Date('2011-07-29'))

'"2011-07-28T22:00:00.000Z"'

The full signature of a toJSON method is as follows:

function (key)

The key parameter allows you to stringify differently, depending on context. It is always a string and indicates where your object was found in the parent object:

Root position

The empty string

Property value

The property key

Array element

The element’s index as a string

I’ll demonstrate toJSON() via the following object:

var obj = {

toJSON: function (key) {

// Use JSON.stringify for nicer-looking output

console.log(JSON.stringify(key));

return 0;

}

};

If you use JSON.stringify(), each occurrence of obj is replaced with 0. The toJSON() method is notified that obj was encountered at the property key 'foo' and at the array index 0:

> JSON.stringify({ foo: obj, bar: [ obj ]})

"foo"

"0"

'{"foo":0,"bar":[0]}'

The built-in toJSON() methods are as follows:

§ Boolean.prototype.toJSON()

§ Number.prototype.toJSON()

§ String.prototype.toJSON()

§ Date.prototype.toJSON()

JSON.parse(text, reviver?)

JSON.parse(text, reviver?) parses the JSON data in text and returns a JavaScript value. Here are some examples:

> JSON.parse("'String'") // illegal quotes

SyntaxError: Unexpected token ILLEGAL

> JSON.parse('"String"')

'String'

> JSON.parse('123')

123

> JSON.parse('[1, 2, 3]')

[ 1, 2, 3 ]

> JSON.parse('{ "hello": 123, "world": 456 }')

{ hello: 123, world: 456 }

The optional parameter reviver is a node visitor (see Transforming Data via Node Visitors) and can be used to transform the parsed data. In this example, we are translating date strings to date objects:

function dateReviver(key, value) {

if (typeof value === 'string') {

var x = Date.parse(value);

if (!isNaN(x)) { // valid date string?

return new Date(x);

}

}

return value;

}

And here is the interaction:

> var str = '{ "name": "John", "birth": "2011-07-28T22:00:00.000Z" }';

> JSON.parse(str, dateReviver)

{ name: 'John', birth: Thu, 28 Jul 2011 22:00:00 GMT }

Transforming Data via Node Visitors

Both JSON.stringify() and JSON.parse() let you transform JavaScript data by passing in a function:

§ JSON.stringify() lets you change the JavaScript data before turning it into JSON.

§ JSON.parse() parses JSON and then lets you post-process the resulting JavaScript data.

The JavaScript data is a tree whose compound nodes are arrays and objects and whose leaves are primitive values (booleans, numbers, strings, null). Let’s use the name node visitor for the transformation function that you pass in. The methods iterate over the tree and call the visitor for each node. It then has the option to replace or delete the node. The node visitor has the signature:

function nodeVisitor(key, value)

The parameters are:

this

The parent of the current node.

key

A key where the current node is located inside its parent. key is always a string.

value

The current node.

The root node root has no parent. When root is visited, a pseudoparent is created for it and the parameters have the following values:

§ this is { '': root }.

§ key is ''.

§ value is root.

The node visitor has three options for returning a value:

§ Return value as it is. Then no change is performed.

§ Return a different value. Then the current node is replaced with it.

§ Return undefined. Then the node is removed.

The following is an example of a node visitor. It logs what values have been passed to it.

function nodeVisitor(key, value) {

console.log([

// Use JSON.stringify for nicer-looking output

JSON.stringify(this), // parent

JSON.stringify(key),

JSON.stringify(value)

].join(' # '));

return value; // don't change node

}

Let’s use this function to examine how the JSON methods iterate over JavaScript data.

JSON.stringify()

The special root node comes first, in a prefix iteration (parent before children). The first node that is visited is always the pseudoroot. The last line that is displayed after each call is the string returned by stringify():

> JSON.stringify(['a','b'], nodeVisitor)

{"":["a","b"]} # "" # ["a","b"]

["a","b"] # "0" # "a"

["a","b"] # "1" # "b"

'["a","b"]'

> JSON.stringify({a:1, b:2}, nodeVisitor)

{"":{"a":1,"b":2}} # "" # {"a":1,"b":2}

{"a":1,"b":2} # "a" # 1

{"a":1,"b":2} # "b" # 2

'{"a":1,"b":2}'

> JSON.stringify('abc', nodeVisitor)

{"":"abc"} # "" # "abc"

'"abc"'

JSON.parse()

The leaves come first, in a postfix iteration (children before parent). The last node that is visited is always the pseudoroot. The last line that is displayed after each call is the JavaScript value returned by parse():

> JSON.parse('["a","b"]', nodeVisitor)

["a","b"] # "0" # "a"

["a","b"] # "1" # "b"

{"":["a","b"]} # "" # ["a","b"]

[ 'a', 'b' ]

> JSON.parse('{"a":1, "b":2}', nodeVisitor)

{"a":1,"b":2} # "a" # 1

{"a":1,"b":2} # "b" # 2

{"":{"a":1,"b":2}} # "" # {"a":1,"b":2}

{ a: 1, b: 2 }

> JSON.parse('"hello"', nodeVisitor)

{"":"hello"} # "" # "hello"

'hello'