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:
{
"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.
Figure 22-1. The front side of the JSON business card shows a logo (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 null
s:
- > 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
isroot
.
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()
- > 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()
- > 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'