JavaScript’s JSON API

[2011-08-06] dev, javascript, jslang
(Ad, please don’t block)
JSON is a plain text data storage format. This blog post describes what it is and how to work with it via an ECMAScript 5 API.

Overview

JSON is an abbreviation of “JavaScript Object Notation”. JSON stores data as plain text. Its grammar is a subset of the grammar of JavaScript expressions. 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, arrays of JSON data
  • Atomic: strings, numbers, booleans, null
Rules:
  • Strings must always be double-quoted, string literals such as 'mystr' are illegal.
  • Property names must be double-quoted.
The specification:
  • Douglas Crockford discovered JSON in 2001 [1]. He gave it a name and put up a specification at json.org. Quote:
    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.
  • 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.
  • Initially, Crockford wanted JSON to have the name “JavaScript Markup Language” [1], but the acronym JSML was already taken by the “JSpeech Markup Language”.

Grammar

Minimalism has always been a core goal for JSON. Its grammar even fits on a business card:

The JSON grammar on a business card. [source]
Transcription:
object
{ }
{ members }
members
pair
pair , members
pair
string : value
array
[ ]
[ elements ]
elements
value
value , elements
value
object
array
string
number
true
false
null
string
""
" chars "
chars
char
char chars
char
<any Unicode character except " or \ or control characters>
\\   \"   \/   \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 JSON API

This section describes several methods for creating and parsing JSON data. First, we have to understand how two of those methods let you hand in a function to customize their operation.

Node visitors

The methods JSON.stringify() and JSON.parse() allow you to customize how they work by passing in a function. Let’s call that function a node visitor. Both methods iterate over a tree of JavaScript values whose compound nodes are arrays and objects and whose leaves are primitive values (booleans, numbers, strings). They call the visitor for each node. It then has the option to replace or delete it.
  • JSON.stringify iterates over a value before stringifying it.
  • JSON.parse iterates over the result of parsing a JSON text.
A node visitor has the following signature:
    function (key, value)
The parameters are:
  • this: the parent of the current node. The root value has a special parent, an object whose only property has the empty string as its name and the root as its value.
  • key: a key where the current node is located inside its parent. key is always a string. It is the empty string if there is no parent.
  • value: the current node.
If a node visitor returns value as it is, no change if performed. If it returns a different value, the current node is replaced with it. If it returns undefined, the node is removed. Example:
    function nodeVisitor(key, value) {
        console.log(
            JSON.stringify(this) // parent
            +"#"+JSON.stringify(key)
            +"#"+JSON.stringify(value));
        return value; // don't change
    }
The above function only uses JSON.stringify(), because it is a good way of displaying JavaScript values. It is used below to examine how iteration works.

Order and kind of nodes visited – stringify. The special root node comes first, in a prefix iteration (parent before children). The last values are the results of the function calls.

    > 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"'
Order and kind of nodes visited – parse. The leaves come first, in a postfix iteration (children before parent). The last values are the results of the function calls.
    > 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'

JSON.stringify(value, [replacer], [space])

translates the JavaScript value value to a text in JSON format. It has two optional arguments:
  • replacer: There are two ways this parameter can influence stringification:
    • Node visitor (see above): replaces nodes in the tree of values. Example:
          function replacer(key, value) {
              if (typeof value === "number") {
                  value = 2 * value;
              }
              return value;
          }
      
      Interaction:
          > JSON.stringify({ a: 5, b: [ 2, 8 ] }, replacer)
          '{"a":10,"b":[4,16]}'
      
    • Whitelist of property names: hides all properties (of non-array objects) that are not in the list. Example:
          JSON.stringify({foo: 1, bar: {foo: 1, bar: 1}}, ["bar"])
      
      returns
          '{"bar":{"bar":1}}'
      
  • space: 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:
    • Number: Multiply the number by the level of indentation and indent the line by as many spaces. Numbers smaller that 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"
            ]
          }
      
    • 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"
          |–]
          }
      
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. Example:
    > JSON.stringify({ toJSON: function() { return "Cool" } })
    '"Cool"'
Dates already have a toJSON method which 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 denotes where in the parent object your object was found:
  • Root position: the empty string.
  • Property value: the property name.
  • Array elemen: the element index as a string.
Example:
    var obj = { toJSON: function(key) { console.log(key) } }
    JSON.stringify({ foo: obj, bar: [ obj ]});
Output:
    foo
    0

JSON.parse(text, [reviver])

parses the JSON data in text and returns a JavaScript value. Examples:
    > JSON.parse("'String'") // illegal single 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 and can be used to transform the parsed data. Example: 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;
    }
Note: You can also use a regular expression to check whether a given string is a date string.
    > JSON.parse(
          '{ "name": "John", "birth": "2011-07-28T22:00:00.000Z" }',
          dateReviver)
    { name: 'John', birth: Thu, 28 Jul 2011 22:00:00 GMT }

Related reading

  1. Video: Douglas Crockford — The JSON Saga [2009-07-02]