buy the book to support the author.

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:

  1. {
  2. "first": "Jane",
  3. "last": "Porter",
  4. "married": true,
  5. "born": 1890,
  6. "friends": [ "Tarzan", "Cheeta" ]
  7. }

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:

  1. function replacer(key, value) {
  2. if (typeof value === 'number') {
  3. value = 2 * value;
  4. }
  5. return value;
  6. }

Using the replacer:

  1. > JSON.stringify({ a: 5, b: [ 2, 8 ] }, replacer)
  2. '{"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:
  1. > JSON.stringify({foo: 1, bar: {foo: 1, bar: 1}}, ['bar'])
  2. '{"bar":{"bar":1}}'

The whitelist has no effect on arrays:

  1. > JSON.stringify(['a', 'b'], ['0'])
  2. '["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:

  1. > console.log(JSON.stringify({a: 0, b: ['\n']}))
  2. {"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:
  1. > console.log(JSON.stringify({a: 0, b: ['\n']}, null, 2))
  2. {
  3. "a": 0,
  4. "b": [
  5. "\n"
  6. ]
  7. }
  • A string
  • To indent, repeat the given string once for each level of indentation. Only the first 10 characters of the string are used:
  1. > console.log(JSON.stringify({a: 0, b: ['\n']}, null, '|--'))
  2. {
  3. |--"a": 0,
  4. |--"b": [
  5. |--|--"\n"
  6. |--]
  7. }

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

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

  1. > var obj = Object.defineProperty({}, 'foo', { enumerable: false, value: 7 });
  2. > Object.getOwnPropertyNames(obj)
  3. [ 'foo' ]
  4. > obj.foo
  5. 7
  6. > JSON.stringify(obj)
  7. '{}'

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:

  1. > JSON.stringify(function () {})
  2. undefined

Properties whose values are unsupported are simply ignored:

  1. > JSON.stringify({ foo: function () {} })
  2. '{}'

Unsupported values in arrays are stringified as nulls:

  1. > JSON.stringify([ function () {} ])
  2. '[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:

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

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

  1. > JSON.stringify(new Date('2011-07-29'))
  2. '"2011-07-28T22:00:00.000Z"'

The full signature of a toJSON method is as follows:

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

  1. var obj = {
  2. toJSON: function (key) {
  3. // Use JSON.stringify for nicer-looking output
  4. console.log(JSON.stringify(key));
  5. return 0;
  6. }
  7. };

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:

  1. > JSON.stringify({ foo: obj, bar: [ obj ]})
  2. "foo"
  3. "0"
  4. '{"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:

  1. > JSON.parse("'String'") // illegal quotes
  2. SyntaxError: Unexpected token ILLEGAL
  3. > JSON.parse('"String"')
  4. 'String'
  5. > JSON.parse('123')
  6. 123
  7. > JSON.parse('[1, 2, 3]')
  8. [ 1, 2, 3 ]
  9. > JSON.parse('{ "hello": 123, "world": 456 }')
  10. { 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:

  1. function dateReviver(key, value) {
  2. if (typeof value === 'string') {
  3. var x = Date.parse(value);
  4. if (!isNaN(x)) { // valid date string?
  5. return new Date(x);
  6. }
  7. }
  8. return value;
  9. }

And here is the interaction:

  1. > var str = '{ "name": "John", "birth": "2011-07-28T22:00:00.000Z" }';
  2. > JSON.parse(str, dateReviver)
  3. { 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:

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

  1. function nodeVisitor(key, value) {
  2. console.log([
  3. // Use JSON.stringify for nicer-looking output
  4. JSON.stringify(this), // parent
  5. JSON.stringify(key),
  6. JSON.stringify(value)
  7. ].join(' # '));
  8. return value; // don't change node
  9. }

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

  1. > JSON.stringify(['a','b'], nodeVisitor)
  2. {"":["a","b"]} # "" # ["a","b"]
  3. ["a","b"] # "0" # "a"
  4. ["a","b"] # "1" # "b"
  5. '["a","b"]'
  6.  
  7. > JSON.stringify({a:1, b:2}, nodeVisitor)
  8. {"":{"a":1,"b":2}} # "" # {"a":1,"b":2}
  9. {"a":1,"b":2} # "a" # 1
  10. {"a":1,"b":2} # "b" # 2
  11. '{"a":1,"b":2}'
  12.  
  13. > JSON.stringify('abc', nodeVisitor)
  14. {"":"abc"} # "" # "abc"
  15. '"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():

  1. > JSON.parse('["a","b"]', nodeVisitor)
  2. ["a","b"] # "0" # "a"
  3. ["a","b"] # "1" # "b"
  4. {"":["a","b"]} # "" # ["a","b"]
  5. [ 'a', 'b' ]
  6.  
  7. > JSON.parse('{"a":1, "b":2}', nodeVisitor)
  8. {"a":1,"b":2} # "a" # 1
  9. {"a":1,"b":2} # "b" # 2
  10. {"":{"a":1,"b":2}} # "" # {"a":1,"b":2}
  11. { a: 1, b: 2 }
  12.  
  13. > JSON.parse('"hello"', nodeVisitor)
  14. {"":"hello"} # "" # "hello"
  15. 'hello'