Native JSON Support in IE8 and Tracking the ECMAScript Fifth Edition Draft Specification

UPDATE
Microsoft has released an update that addresses most of the issues discussed below. Refer KB976662 for more information about the update. The MSDN links for JSON documentation - JSON.stringifyJSON.parse and toJSON, have also been updated to reflect these changes.

Suresh Jayabalan, Program Manager, JScript

=============================

Internet Explorer 8 was the first browser to introduce native support for encoding and decoding JSON. While we were finalizing the JSON support to be introduced in IE8, the ECMAScript Fifth Edition (ES5) Draft Specification was still under active development.

At that time, there were clearly two options in front of us:

First, to wait till the new specification is approved so that we can ship a fully compliant native JSON feature. This had a big disadvantage of not being able to provide web developers with both – performance and security benefits that native JSON offers over a script implementation of JSON in IE8 (as final approval of the standard was at least a year away).

Second, providing JSON support in IE8 based upon early drafts of the standard and then, if necessary, bringing the IE JSON support into compliance with the ES5 specification once the standard is ratified (currently expected to happen in Dec’09). This option had the disadvantage that if the specification changed after IE8 shipped there would be a period time during which the IE8 JSON support would have variances from the ES5 specification.

Weighing the two, we decided to choose the second option as it meant providing web developers the ability to start taking advantage of the native JSON constructs immediately. Moreover, the changes that were expected in the specification were not drastic and could easily be detected and worked around as needed by providing simple wrappers created in JScript itself.

Both, the candidate draft of the ES5 Specification and IE8 are now available. The list of differences that currently exist between the implementation and the draft specification is provided below with possible workarounds. It is worth noting that additional changes may still be made to the candidate ES5 specification based upon reviewer feedback. What this means is that the final form factor of JSON might be a bit different than the draft or what is covered in this article by the time ES5 is standardized.

Following is a list of differences between IE8’s behavior and the current ES5 Draft Specification of

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

  • If value is undefined and is not replaced using a replacer function, stringify should return undefined instead of the string "undefined".
  • If value is a function and is not replaced using a replacer function, stringify should return the value undefined rather than the string "undefined".
  • If value is a Number or String object after the toJSON() method and replacer function have been executed on value, stringify should return the primitive number or string value stored in the Number or String object.  Note: Per the latest ES5 errata (27 May’09) Boolean objects are also included in this.
  • If value is a cyclic object or array, stringify should throw a TypeError and not an Error.
  • If replacer is not a Function or an Array, stringify should ignore the replacer parameter and not throw an error.
  • If the replacer function calls a DOM method and passes the key parameter to the same, stringify should not throw an Error. Note: To work around this issue, use key.toString() when passing key to DOM methods like alert() and createElement() inside the replacer function.
  • If space is an empty string, stringify should default space to an empty string instead of newline.
  • If space is a Number object or a String object, stringify should use the primitive value of the objects instead of ignoring the same.
  • If space is set to a value greater than 100, stringify should default space to be 100 instead of the current maximum limit of 65535 after which space is defaulted to 0. Note: Per the latest ES5 errata (27 May’09) the max space should be defaulted to 10.
  • If space is a non integer numeric value, stringify should default space to be the integer part of the space value instead of defaulting it to the ceiling value of the space argument.

JSON.parse (text[, reviver])

  • If text contains control characters other than TAB, CR, or LF, throw a Syntax Error. In addition, throw a SyntaxError if TAB, CR, or LF appear in a JSON string literal. IE8 correctly rejects illegal characters outside of string literals. Note: To work around this problem for string literals, user can define a reviver method that looks for string values containing control characters. Given the performance tradeoff to find these characters in all JSON text strings, a generic reviver is not provided in the sample below.

Date.prototype.toJSON (key)

  • If the Date value is not a finite Number, toJSON should return null

Note that the majority of these differences concerning the handling of invalid argument values or other unusual situations. Most of them have no impact on the encoding or decoding of normal data.

A workaround for these differences is provided in the following patchJSON function which can be called to suitably modify the built-in JSON support methods, before using any of the JSON built-ins by a user. It places a wrapper around the built-in JSON functions.

  1. function patchJSON() {

  2.     // Check if JSON exists, or if

  3.     // JSON has already been patched or updated

  4.     if ((!this.JSON) || (JSON.stringify(undefined) !== 'undefined'))

  5.         return false;  //JSON has already been patched or updated;

  6.     var builtinStringify = JSON.stringify;

  7.     JSON.stringify = newStringify;

  8.     // Date.prototype.toJSON returns null for Dates with a non finite value

  9.     var origDateToJSON = Date.prototype.toJSON;

  10.     if ((origDateToJSON.toString()) ===

  11.     "\nfunction toJSON() {\n [native code]\n}\n") {

  12.         Date.prototype.toJSON = function(value) {

  13.             if (isNaN(this.valueOf())) return null;

  14.             else {

  15.                 return origDateToJSON.call(this, value);

  16.             }

  17.         }

  18.     }

  19.     return true;

  20.     function newStringify(value, replacer, space) {

  21.         // Return undefined when value parameter is undefined

  22.         // or when toJSON exists and returns undefined

  23.         if ((value === undefined) && !(replacer)) return undefined;

  24.         if (value && typeof value === 'object' &&

  25.                 typeof value.toJSON === 'function' &&

  26.                 !replacer && value.toJSON("") === undefined)

  27.             return undefined;

  28.         // Return undefined when value paramenter is a function

  29.         if ((typeof value === "function") && !(replacer)) return undefined;

  30.         // Ignore the replacer and do not throw an error if the replacer

  31.         // is not a function or array.

  32.         if (replacer && (typeof replacer !== "function")

  33.             && !(replacer instanceof Array))

  34.             replacer = null;

  35.         if (space !== undefined) {

  36.             // Treat empty string as no space

  37.             if (space === "") space = 0;

  38.             else if (typeof space === "object") {

  39.                 // If space is a Number object or String object

  40.                 // use it's primitive value

  41.                 if ((space instanceof Number) || (space instanceof String))

  42.                     space = space.valueOf();

  43.                 // Ignore objects other than Number and Strings

  44.                 else space = 0;

  45.             }

  46.             // If space is set to a value greater than 10, space should be

  47.             // defaulted to 10. If space is not integral, use the floor value

  48.             // If space string has a length greater than 10,

  49.             // space should be defaulted to first 10 characters

  50.             if (typeof space === "number") space = Math.min(10,

  51.                 Math.floor(Math.abs(space)));

  52.             else if (typeof space === "string") {

  53.                 if (space.length > 10)

  54.                     space = space.slice(0, 10);

  55.             }

  56.             else space = 0;

  57.         }

  58.         // Return primitive number, string or boolean value stored

  59.         // in the Number, String or Boolean object

  60.         function newReplacer(key, value) {

  61.             // Execute the replacerand get it's return value

  62.             var tmpValue = replacer.call(this, key, value);

  63.             // If value is a Number or String or Boolean object

  64.             // use its primitive value

  65.             if (typeof tmpValue === "object") {

  66.                 if ((tmpValue instanceof Number) || (tmpValue instanceof String) ||

  67.                 (tmpValue instanceof Boolean)) tmpValue = tmpValue.valueOf();

  68.             }

  69.             return tmpValue;

  70.         }

  71.         try {

  72.             // Check if the replacer returns undefined or a function

  73.             // If it does, return undefined

  74.             var tmpValue = value;

  75.             if (replacer && typeof replacer === "function") {

  76.                 tmpValue = newReplacer.call({ "": tmpValue }, "", tmpValue);

  77.                 if ((tmpValue === undefined) ||

  78.                 (typeof tmpValue == 'function')) return undefined;

  79.                 return builtinStringify(value, newReplacer, space);

  80.             }

  81.             else return builtinStringify(value, replacer, space);

  82.         }

  83.         // Throw a Type Error if value is a cyclic objects

  84.         catch (e) {

  85.             var replacementException = e;

  86.             if (e.number === -2146823254) {

  87.                 replacementException = new TypeError();

  88.                 replacementException.description = e.description;

  89.                 replacementException.message = e.message;

  90.                 replacementException.number = e.number;

  91.             }

  92.             throw replacementException;

  93.         }

  94.     }

  95. }

Because the above patch primarily deals with errors that arise from improperly calling the JSON functions it is most useful while you are debugging your code. When you code is ready for production you may not need to use.

Note: Apart from the above differences due to the changes in the ES5 draft specifications for JSON, there is a known issue which currently exists for IE8. The details of the same along with the possible workarounds are covered in this blog post.

In case you will like to follow the discussions by the TC39 members around how the standard is taking shape, and the discussion around the changes for JSON, you can refer the discussion archives here. The latest ES5 drafts are available at the ECMAScript wiki.

Gaurav Seth, Program Manager, JScript