# lib/goog/json/json.js

1 | // Copyright 2006 The Closure Library Authors. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS-IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | /**
16 | * @fileoverview JSON utility functions.
17 | * @author arv@google.com (Erik Arvidsson)
18 | */
19 |
20 |
21 | goog.provide('goog.json');
22 | goog.provide('goog.json.Replacer');
23 | goog.provide('goog.json.Reviver');
24 | goog.provide('goog.json.Serializer');
25 |
26 |
27 | /**
28 | * @define {boolean} If true, use the native JSON parsing API.
29 | * NOTE(ruilopes): EXPERIMENTAL, handle with care. Setting this to true might
30 | * break your code. The default {@code goog.json.parse} implementation is able
31 | * to handle invalid JSON, such as JSPB.
32 | */
33 | goog.define('goog.json.USE_NATIVE_JSON', false);
34 |
35 |
36 | /**
37 | * Tests if a string is an invalid JSON string. This only ensures that we are
38 | * not using any invalid characters
39 | * @param {string} s The string to test.
40 | * @return {boolean} True if the input is a valid JSON string.
41 | */
42 | goog.json.isValid = function(s) {
43 | // All empty whitespace is not valid.
44 | if (/^\s*$/.test(s)) {
45 | return false;
46 | }
47 |
48 | // This is taken from http://www.json.org/json2.js which is released to the
49 | // public domain.
50 | // Changes: We dissallow \u2028 Line separator and \u2029 Paragraph separator
51 | // inside strings. We also treat \u2028 and \u2029 as whitespace which they
52 | // are in the RFC but IE and Safari does not match \s to these so we need to
53 | // include them in the reg exps in all places where whitespace is allowed.
54 | // We allowed \x7f inside strings because some tools don't escape it,
55 | // e.g. http://www.json.org/java/org/json/JSONObject.java
56 |
57 | // Parsing happens in three stages. In the first stage, we run the text
58 | // against regular expressions that look for non-JSON patterns. We are
59 | // especially concerned with '()' and 'new' because they can cause invocation,
60 | // and '=' because it can cause mutation. But just to be safe, we want to
61 | // reject all unexpected forms.
62 |
63 | // We split the first stage into 4 regexp operations in order to work around
64 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
65 | // replace all backslash pairs with '@' (a non-JSON character). Second, we
66 | // replace all simple value tokens with ']' characters. Third, we delete all
67 | // open brackets that follow a colon or comma or that begin the text. Finally,
68 | // we look to see that the remaining characters are only whitespace or ']' or
69 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
70 |
71 | // Don't make these static since they have the global flag.
72 | var backslashesRe = /\\["\\\/bfnrtu]/g;
73 | var simpleValuesRe =
74 | /"[^"\\\n\r\u2028\u2029\x00-\x08\x0a-\x1f]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
75 | var openBracketsRe = /(?:^|:|,)(?:[\s\u2028\u2029]*\[)+/g;
76 | var remainderRe = /^[\],:{}\s\u2028\u2029]*$/;
77 |
78 | return remainderRe.test(s.replace(backslashesRe, '@').
79 | replace(simpleValuesRe, ']').
80 | replace(openBracketsRe, ''));
81 | };
82 |
83 |
84 | /**
85 | * Parses a JSON string and returns the result. This throws an exception if
86 | * the string is an invalid JSON string.
87 | *
88 | * Note that this is very slow on large strings. If you trust the source of
89 | * the string then you should use unsafeParse instead.
90 | *
91 | * @param {*} s The JSON string to parse.
92 | * @throws Error if s is invalid JSON.
93 | * @return {Object} The object generated from the JSON string, or null.
94 | */
95 | goog.json.parse = goog.json.USE_NATIVE_JSON ?
96 | /** @type {function(*):Object} */ (goog.global['JSON']['parse']) :
97 | function(s) {
98 | var o = String(s);
99 | if (goog.json.isValid(o)) {
100 | /** @preserveTry */
101 | try {
102 | return /** @type {Object} */ (eval('(' + o + ')'));
103 | } catch (ex) {
104 | }
105 | }
106 | throw Error('Invalid JSON string: ' + o);
107 | };
108 |
109 |
110 | /**
111 | * Parses a JSON string and returns the result. This uses eval so it is open
112 | * to security issues and it should only be used if you trust the source.
113 | *
114 | * @param {string} s The JSON string to parse.
115 | * @return {Object} The object generated from the JSON string.
116 | */
117 | goog.json.unsafeParse = goog.json.USE_NATIVE_JSON ?
118 | /** @type {function(string):Object} */ (goog.global['JSON']['parse']) :
119 | function(s) {
120 | return /** @type {Object} */ (eval('(' + s + ')'));
121 | };
122 |
123 |
124 | /**
125 | * JSON replacer, as defined in Section 15.12.3 of the ES5 spec.
126 | * @see http://ecma-international.org/ecma-262/5.1/#sec-15.12.3
127 | *
128 | * TODO(nicksantos): Array should also be a valid replacer.
129 | *
130 | * @typedef {function(this:Object, string, *): *}
131 | */
132 | goog.json.Replacer;
133 |
134 |
135 | /**
136 | * JSON reviver, as defined in Section 15.12.2 of the ES5 spec.
137 | * @see http://ecma-international.org/ecma-262/5.1/#sec-15.12.3
138 | *
139 | * @typedef {function(this:Object, string, *): *}
140 | */
141 | goog.json.Reviver;
142 |
143 |
144 | /**
145 | * Serializes an object or a value to a JSON string.
146 | *
147 | * @param {*} object The object to serialize.
148 | * @param {?goog.json.Replacer=} opt_replacer A replacer function
149 | * called for each (key, value) pair that determines how the value
150 | * should be serialized. By defult, this just returns the value
151 | * and allows default serialization to kick in.
152 | * @throws Error if there are loops in the object graph.
153 | * @return {string} A JSON string representation of the input.
154 | */
155 | goog.json.serialize = goog.json.USE_NATIVE_JSON ?
156 | /** @type {function(*, ?goog.json.Replacer=):string} */
157 | (goog.global['JSON']['stringify']) :
158 | function(object, opt_replacer) {
159 | // NOTE(nicksantos): Currently, we never use JSON.stringify.
160 | //
161 | // The last time I evaluated this, JSON.stringify had subtle bugs and
162 | // behavior differences on all browsers, and the performance win was not
163 | // large enough to justify all the issues. This may change in the future
164 | // as browser implementations get better.
165 | //
166 | // assertSerialize in json_test contains if branches for the cases
167 | // that fail.
168 | return new goog.json.Serializer(opt_replacer).serialize(object);
169 | };
170 |
171 |
172 |
173 | /**
174 | * Class that is used to serialize JSON objects to a string.
175 | * @param {?goog.json.Replacer=} opt_replacer Replacer.
176 | * @constructor
177 | */
178 | goog.json.Serializer = function(opt_replacer) {
179 | /**
180 | * @type {goog.json.Replacer|null|undefined}
181 | * @private
182 | */
183 | this.replacer_ = opt_replacer;
184 | };
185 |
186 |
187 | /**
188 | * Serializes an object or a value to a JSON string.
189 | *
190 | * @param {*} object The object to serialize.
191 | * @throws Error if there are loops in the object graph.
192 | * @return {string} A JSON string representation of the input.
193 | */
194 | goog.json.Serializer.prototype.serialize = function(object) {
195 | var sb = [];
196 | this.serializeInternal(object, sb);
197 | return sb.join('');
198 | };
199 |
200 |
201 | /**
202 | * Serializes a generic value to a JSON string
203 | * @protected
204 | * @param {*} object The object to serialize.
205 | * @param {Array<string>} sb Array used as a string builder.
206 | * @throws Error if there are loops in the object graph.
207 | */
208 | goog.json.Serializer.prototype.serializeInternal = function(object, sb) {
209 | if (object == null) {
210 | // undefined == null so this branch covers undefined as well as null
211 | sb.push('null');
212 | return;
213 | }
214 |
215 | if (typeof object == 'object') {
216 | if (goog.isArray(object)) {
217 | this.serializeArray(object, sb);
218 | return;
219 | } else if (object instanceof String ||
220 | object instanceof Number ||
221 | object instanceof Boolean) {
222 | object = object.valueOf();
223 | // Fall through to switch below.
224 | } else {
225 | this.serializeObject_(/** @type {Object} */ (object), sb);
226 | return;
227 | }
228 | }
229 |
230 | switch (typeof object) {
231 | case 'string':
232 | this.serializeString_(object, sb);
233 | break;
234 | case 'number':
235 | this.serializeNumber_(object, sb);
236 | break;
237 | case 'boolean':
238 | sb.push(String(object));
239 | break;
240 | case 'function':
241 | sb.push('null');
242 | break;
243 | default:
244 | throw Error('Unknown type: ' + typeof object);
245 | }
246 | };
247 |
248 |
249 | /**
250 | * Character mappings used internally for goog.string.quote
251 | * @private
252 | * @type {!Object}
253 | */
254 | goog.json.Serializer.charToJsonCharCache_ = {
255 | '\"': '\\"',
256 | '\\': '\\\\',
257 | '/': '\\/',
258 | '\b': '\\b',
259 | '\f': '\\f',
260 | '\n': '\\n',
261 | '\r': '\\r',
262 | '\t': '\\t',
263 |
264 | '\x0B': '\\u000b' // '\v' is not supported in JScript
265 | };
266 |
267 |
268 | /**
269 | * Regular expression used to match characters that need to be replaced.
270 | * The S60 browser has a bug where unicode characters are not matched by
271 | * regular expressions. The condition below detects such behaviour and
272 | * adjusts the regular expression accordingly.
273 | * @private
274 | * @type {!RegExp}
275 | */
276 | goog.json.Serializer.charsToReplace_ = /\uffff/.test('\uffff') ?
277 | /[\\\"\x00-\x1f\x7f-\uffff]/g : /[\\\"\x00-\x1f\x7f-\xff]/g;
278 |
279 |
280 | /**
281 | * Serializes a string to a JSON string
282 | * @private
283 | * @param {string} s The string to serialize.
284 | * @param {Array<string>} sb Array used as a string builder.
285 | */
286 | goog.json.Serializer.prototype.serializeString_ = function(s, sb) {
287 | // The official JSON implementation does not work with international
288 | // characters.
289 | sb.push('"', s.replace(goog.json.Serializer.charsToReplace_, function(c) {
290 | // caching the result improves performance by a factor 2-3
291 | var rv = goog.json.Serializer.charToJsonCharCache_[c];
292 | if (!rv) {
293 | rv = '\\u' + (c.charCodeAt(0) | 0x10000).toString(16).substr(1);
294 | goog.json.Serializer.charToJsonCharCache_[c] = rv;
295 | }
296 | return rv;
297 | }), '"');
298 | };
299 |
300 |
301 | /**
302 | * Serializes a number to a JSON string
303 | * @private
304 | * @param {number} n The number to serialize.
305 | * @param {Array<string>} sb Array used as a string builder.
306 | */
307 | goog.json.Serializer.prototype.serializeNumber_ = function(n, sb) {
308 | sb.push(isFinite(n) && !isNaN(n) ? String(n) : 'null');
309 | };
310 |
311 |
312 | /**
313 | * Serializes an array to a JSON string
314 | * @param {Array<string>} arr The array to serialize.
315 | * @param {Array<string>} sb Array used as a string builder.
316 | * @protected
317 | */
318 | goog.json.Serializer.prototype.serializeArray = function(arr, sb) {
319 | var l = arr.length;
320 | sb.push('[');
321 | var sep = '';
322 | for (var i = 0; i < l; i++) {
323 | sb.push(sep);
324 |
325 | var value = arr[i];
326 | this.serializeInternal(
327 | this.replacer_ ? this.replacer_.call(arr, String(i), value) : value,
328 | sb);
329 |
330 | sep = ',';
331 | }
332 | sb.push(']');
333 | };
334 |
335 |
336 | /**
337 | * Serializes an object to a JSON string
338 | * @private
339 | * @param {Object} obj The object to serialize.
340 | * @param {Array<string>} sb Array used as a string builder.
341 | */
342 | goog.json.Serializer.prototype.serializeObject_ = function(obj, sb) {
343 | sb.push('{');
344 | var sep = '';
345 | for (var key in obj) {
346 | if (Object.prototype.hasOwnProperty.call(obj, key)) {
347 | var value = obj[key];
348 | // Skip functions.
349 | if (typeof value != 'function') {
350 | sb.push(sep);
351 | this.serializeString_(key, sb);
352 | sb.push(':');
353 |
354 | this.serializeInternal(
355 | this.replacer_ ? this.replacer_.call(obj, key, value) : value,
356 | sb);
357 |
358 | sep = ',';
359 | }
360 | }
361 | }
362 | sb.push('}');
363 | };