├── .prettierrc.json ├── .travis.yml ├── .gitignore ├── index.js ├── package.json ├── test ├── string-option-test.js ├── strict-option-test.js ├── bigint-stringify-test.js ├── bigint-test.js ├── proto-test.js └── bigint-parse-test.js ├── LICENSE ├── README.md └── lib ├── parse.js └── stringify.js /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '12' 5 | - '13' 6 | - '14' 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | .*.swp 10 | node_modules 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var json_stringify = require('./lib/stringify.js').stringify; 2 | var json_parse = require('./lib/parse.js'); 3 | 4 | module.exports = function(options) { 5 | return { 6 | parse: json_parse(options), 7 | stringify: json_stringify 8 | } 9 | }; 10 | //create the default method members with no options applied for backwards compatibility 11 | module.exports.parse = json_parse(); 12 | module.exports.stringify = json_stringify; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-bigint-fixes", 3 | "version": "1.1.0", 4 | "description": "JSON.parse with bigints support", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js", 8 | "lib/parse.js", 9 | "lib/stringify.js" 10 | ], 11 | "scripts": { 12 | "test": "./node_modules/mocha/bin/mocha -R spec --check-leaks test/*-test.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:sidorares/json-bigint.git" 17 | }, 18 | "keywords": [ 19 | "JSON", 20 | "bigint", 21 | "bignumber", 22 | "parse", 23 | "json" 24 | ], 25 | "author": "Andrey Sidorov ", 26 | "license": "MIT", 27 | "dependencies": { 28 | "bignumber.js": "^9.0.0" 29 | }, 30 | "devDependencies": { 31 | "chai": "4.2.0", 32 | "mocha": "8.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/string-option-test.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha') 2 | , assert = require('chai').assert 3 | , expect = require('chai').expect 4 | ; 5 | 6 | describe("Testing 'storeAsString' option", function(){ 7 | var key = '{ "key": 12345678901234567 }'; 8 | it("Should show that the key is of type object", function(done){ 9 | var JSONbig = require('../index'); 10 | var result = JSONbig.parse(key); 11 | expect(typeof result.key).to.equal("object"); 12 | done(); 13 | }); 14 | 15 | it("Should show that key is of type string, when storeAsString option is true", function(done){ 16 | var JSONstring = require('../index')({"storeAsString": true}); 17 | var result = JSONstring.parse(key); 18 | expect(typeof result.key).to.equal("string"); 19 | done(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/strict-option-test.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha'), 2 | assert = require('chai').assert, 3 | expect = require('chai').expect; 4 | describe("Testing 'strict' option", function () { 5 | var dupkeys = '{ "dupkey": "value 1", "dupkey": "value 2"}'; 6 | it('Should show that duplicate keys just get overwritten by default', function (done) { 7 | var JSONbig = require('../index'); 8 | var result = 'before'; 9 | function tryParse() { 10 | result = JSONbig.parse(dupkeys); 11 | } 12 | expect(tryParse).to.not.throw('anything'); 13 | expect(result.dupkey).to.equal('value 2'); 14 | done(); 15 | }); 16 | 17 | it("Should show that the 'strict' option will fail-fast on duplicate keys", function (done) { 18 | var JSONstrict = require('../index')({ strict: true }); 19 | var result = 'before'; 20 | function tryParse() { 21 | result = JSONstrict.parse(dupkeys); 22 | } 23 | 24 | expect(tryParse).to.throw('Duplicate key "dupkey"'); 25 | expect(result).to.equal('before'); 26 | done(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Andrey Sidorov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/bigint-stringify-test.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha') 2 | , assert = require('chai').assert 3 | , expect = require('chai').expect 4 | , BigNumber = require('bignumber.js') 5 | ; 6 | 7 | describe("Testing native BigInt support: stringify", function () { 8 | if (typeof (BigInt) === 'undefined') { 9 | console.log('No native BigInt'); 10 | return; 11 | } 12 | it("Should show JSONbig can stringify native BigInt", function (done) { 13 | var JSONbig = require('../index'); 14 | var obj = { 15 | // We cannot use n-literals - otherwise older NodeJS versions fail on this test 16 | big: eval("123456789012345678901234567890n"), 17 | small: -42, 18 | bigConstructed: BigInt(1), 19 | smallConstructed: Number(2), 20 | }; 21 | expect(obj.small.toString(), "string from small int").to.equal("-42"); 22 | expect(obj.big.toString(), "string from big int").to.equal("123456789012345678901234567890"); 23 | expect(typeof obj.big, "typeof big int").to.equal('bigint'); 24 | 25 | var output = JSONbig.stringify(obj); 26 | expect(output).to.equal( 27 | '{' + 28 | '"big":123456789012345678901234567890,' + 29 | '"small":-42,' + 30 | '"bigConstructed":1,' + 31 | '"smallConstructed":2' + 32 | '}' 33 | ); 34 | done(); 35 | }); 36 | }); -------------------------------------------------------------------------------- /test/bigint-test.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha') 2 | , assert = require('chai').assert 3 | , expect = require('chai').expect 4 | , BigNumber = require('bignumber.js') 5 | ; 6 | 7 | describe("Testing bigint support", function(){ 8 | var input = '{"big":9223372036854775807,"small":123}'; 9 | 10 | it("Should show classic JSON.parse lacks bigint support", function(done){ 11 | var obj = JSON.parse(input); 12 | expect(obj.small.toString(), "string from small int").to.equal("123"); 13 | expect(obj.big.toString(), "string from big int").to.not.equal("9223372036854775807"); 14 | 15 | var output = JSON.stringify(obj); 16 | expect(output).to.not.equal(input); 17 | done(); 18 | }); 19 | 20 | it("Should show JSONbig does support bigint parse/stringify roundtrip", function(done){ 21 | var JSONbig = require('../index'); 22 | var obj = JSONbig.parse(input); 23 | expect(obj.small.toString(), "string from small int").to.equal("123"); 24 | expect(obj.big.toString(), "string from big int").to.equal("9223372036854775807"); 25 | expect(obj.big, "instanceof big int").to.be.instanceof(BigNumber); 26 | 27 | var output = JSONbig.stringify(obj); 28 | expect(output).to.equal(input); 29 | done(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/proto-test.js: -------------------------------------------------------------------------------- 1 | const makeJSON = require('../index.js'); 2 | const expect = require('chai').expect; 3 | describe('__proto__ and constructor assignment', function () { 4 | it('should set __proto__ property but not a prototype if protoAction is set to preserve', () => { 5 | const JSONbig = makeJSON({ protoAction: 'preserve' }); 6 | const obj1 = JSONbig.parse('{ "__proto__": 1000000000000000 }'); 7 | expect(Object.getPrototypeOf(obj1)).to.equal(null); 8 | const obj2 = JSONbig.parse('{ "__proto__": { "admin": true } }'); 9 | expect(obj2.admin).to.not.equal(true); 10 | }); 11 | 12 | it('should throw an exception if protoAction set to invalid value', () => { 13 | expect(() => { 14 | makeJSON({ protoAction: 'invalid value' }); 15 | }).to.throw( 16 | 'Incorrect value for protoAction option, must be "error", "ignore" or undefined but passed invalid value' 17 | ); 18 | }); 19 | 20 | it('should throw an exception if constructorAction set to invalid value', () => { 21 | expect(() => { 22 | makeJSON({ constructorAction: 'invalid value' }); 23 | }).to.throw( 24 | 'Incorrect value for constructorAction option, must be "error", "ignore" or undefined but passed invalid value' 25 | ); 26 | }); 27 | 28 | it('should throw an exception if protoAction set to error and there is __proto__ property', () => { 29 | const JSONbig = makeJSON({ protoAction: 'error' }); 30 | expect(() => 31 | JSONbig.parse('{ "\\u005f_proto__": 1000000000000000 }') 32 | ).to.throw('Object contains forbidden prototype property'); 33 | }); 34 | 35 | it('should throw an exception if constructorAction set to error and there is constructor property', () => { 36 | const JSONbig = makeJSON({ protoAction: 'error' }); 37 | expect(() => JSONbig.parse('{ "constructor": 1000000000000000 }')).to.throw( 38 | 'Object contains forbidden constructor property' 39 | ); 40 | }); 41 | 42 | it('should ignore __proto__ property if protoAction is set to ignore', () => { 43 | const JSONbig = makeJSON({ protoAction: 'ignore' }); 44 | const obj1 = JSONbig.parse( 45 | '{ "__proto__": 1000000000000000, "a" : 42, "nested": { "__proto__": false, "b": 43 } }' 46 | ); 47 | expect(Object.getPrototypeOf(obj1)).to.equal(null); 48 | expect(obj1).to.deep.equal({ a: 42, nested: { b: 43 } }); 49 | }); 50 | 51 | it('should ignore constructor property if constructorAction is set to ignore', () => { 52 | const JSONbig = makeJSON({ constructorAction: 'ignore' }); 53 | const obj1 = JSONbig.parse( 54 | '{ "constructor": 1000000000000000, "a" : 42, "nested": { "constructor": false, "b": 43 } }' 55 | ); 56 | expect(obj1).to.deep.equal({ a: 42, nested: { b: 43 } }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/bigint-parse-test.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha') 2 | , assert = require('chai').assert 3 | , expect = require('chai').expect 4 | , BigNumber = require('bignumber.js') 5 | ; 6 | 7 | describe("Testing native BigInt support: parse", function () { 8 | if (typeof (BigInt) === 'undefined') { 9 | console.log('No native BigInt'); 10 | return; 11 | } 12 | var input = '{"big":92233720368547758070,"small":123,"deci":1234567890.0123456,"shortExp":1.79e+308,"longExp":1.7976931348623157e+308}'; 13 | 14 | it("Should show JSONbig does support parsing native BigInt", function (done) { 15 | var JSONbig = require('../index')({ 16 | "useNativeBigInt": true 17 | }); 18 | var obj = JSONbig.parse(input); 19 | expect(obj.small, "small int").to.equal(123); 20 | expect(obj.big.toString(), "big int").to.equal("92233720368547758070"); 21 | expect(typeof obj.big, "big int").to.equal('bigint'); 22 | done(); 23 | }); 24 | 25 | it("Should show JSONbig does support forced parsing to native BigInt", function (done) { 26 | var JSONbig = require('../index')({ 27 | "alwaysParseAsBig": true, 28 | "useNativeBigInt": true 29 | }); 30 | var obj = JSONbig.parse(input); 31 | expect(obj.big.toString(), "big int").to.equal("92233720368547758070"); 32 | expect(typeof obj.big, "big int").to.equal('bigint'); 33 | expect(obj.small.toString(), "small int").to.equal("123"); 34 | expect(typeof obj.small, "small int").to.equal('bigint'); 35 | done(); 36 | }); 37 | 38 | it("Should show JSONbig does support decimal and scientific notation parse/stringify roundtrip", function (done) { 39 | var JSONbig = require('../index')({ 40 | "useNativeBigInt": true 41 | }); 42 | var obj = JSONbig.parse(input); 43 | expect(obj.deci.toString(), "decimal number").to.equal("1234567890.0123456"); 44 | expect(typeof obj.deci, "decimal number").to.equal('number'); 45 | expect(obj.shortExp.toString(), "short exponential number").to.equal("1.79e+308"); 46 | expect(typeof obj.shortExp, "short exponential number").to.equal('number'); 47 | expect(obj.longExp.toString(), "long exponential number").to.equal("1.7976931348623157e+308"); 48 | expect(typeof obj.longExp, "long exponential number").to.equal('number'); 49 | var output = JSONbig.stringify(obj); 50 | expect(output).to.equal(input); 51 | done(); 52 | }); 53 | 54 | it("Should show JSONbig does support native Bigint parse/stringify roundtrip", function (done) { 55 | var JSONbig = require('../index')({ 56 | "useNativeBigInt": true 57 | }); 58 | var obj = JSONbig.parse(input); 59 | var output = JSONbig.stringify(obj); 60 | expect(output).to.equal(input); 61 | done(); 62 | }); 63 | 64 | it("Should show JSONbig does support native Bigint parse/stringify roundtrip when BigInt is forced", function (done) { 65 | var JSONbig = require('../index')({ 66 | "alwaysParseAsBig": true, 67 | "useNativeBigInt": true 68 | }); 69 | var obj = JSONbig.parse(input); 70 | var output = JSONbig.stringify(obj); 71 | expect(output).to.equal(input); 72 | done(); 73 | }); 74 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-bigint 2 | 3 | [![Build Status](https://secure.travis-ci.org/sidorares/json-bigint.png)](http://travis-ci.org/sidorares/json-bigint) 4 | [![NPM](https://nodei.co/npm/json-bigint.png?downloads=true&stars=true)](https://nodei.co/npm/json-bigint/) 5 | 6 | JSON.parse/stringify with bigints support. Based on Douglas Crockford [JSON.js](https://github.com/douglascrockford/JSON-js) package and [bignumber.js](https://github.com/MikeMcl/bignumber.js) library. 7 | 8 | Native `Bigint` was added to JS recently, so we added an option to leverage it instead of `bignumber.js`. However, the parsing with native `BigInt` is kept an option for backward compability. 9 | 10 | While most JSON parsers assume numeric values have same precision restrictions as IEEE 754 double, JSON specification _does not_ say anything about number precision. Any floating point number in decimal (optionally scientific) notation is valid JSON value. It's a good idea to serialize values which might fall out of IEEE 754 integer precision as strings in your JSON api, but `{ "value" : 9223372036854775807}`, for example, is still a valid RFC4627 JSON string, and in most JS runtimes the result of `JSON.parse` is this object: `{ value: 9223372036854776000 }` 11 | 12 | ========== 13 | 14 | example: 15 | 16 | ```js 17 | var JSONbig = require('json-bigint'); 18 | 19 | var json = '{ "value" : 9223372036854775807, "v2": 123 }'; 20 | console.log('Input:', json); 21 | console.log(''); 22 | 23 | console.log('node.js built-in JSON:'); 24 | var r = JSON.parse(json); 25 | console.log('JSON.parse(input).value : ', r.value.toString()); 26 | console.log('JSON.stringify(JSON.parse(input)):', JSON.stringify(r)); 27 | 28 | console.log('\n\nbig number JSON:'); 29 | var r1 = JSONbig.parse(json); 30 | console.log('JSONbig.parse(input).value : ', r1.value.toString()); 31 | console.log('JSONbig.stringify(JSONbig.parse(input)):', JSONbig.stringify(r1)); 32 | ``` 33 | 34 | Output: 35 | 36 | ``` 37 | Input: { "value" : 9223372036854775807, "v2": 123 } 38 | 39 | node.js built-in JSON: 40 | JSON.parse(input).value : 9223372036854776000 41 | JSON.stringify(JSON.parse(input)): {"value":9223372036854776000,"v2":123} 42 | 43 | 44 | big number JSON: 45 | JSONbig.parse(input).value : 9223372036854775807 46 | JSONbig.stringify(JSONbig.parse(input)): {"value":9223372036854775807,"v2":123} 47 | ``` 48 | 49 | ### Options 50 | 51 | The behaviour of the parser is somewhat configurable through 'options' 52 | 53 | #### options.strict, boolean, default false 54 | 55 | Specifies the parsing should be "strict" towards reporting duplicate-keys in the parsed string. 56 | The default follows what is allowed in standard json and resembles the behavior of JSON.parse, but overwrites any previous values with the last one assigned to the duplicate-key. 57 | 58 | Setting options.strict = true will fail-fast on such duplicate-key occurances and thus warn you upfront of possible lost information. 59 | 60 | example: 61 | 62 | ```js 63 | var JSONbig = require('json-bigint'); 64 | var JSONstrict = require('json-bigint')({ strict: true }); 65 | 66 | var dupkeys = '{ "dupkey": "value 1", "dupkey": "value 2"}'; 67 | console.log('\n\nDuplicate Key test with both lenient and strict JSON parsing'); 68 | console.log('Input:', dupkeys); 69 | var works = JSONbig.parse(dupkeys); 70 | console.log('JSON.parse(dupkeys).dupkey: %s', works.dupkey); 71 | var fails = 'will stay like this'; 72 | try { 73 | fails = JSONstrict.parse(dupkeys); 74 | console.log('ERROR!! Should never get here'); 75 | } catch (e) { 76 | console.log( 77 | 'Succesfully catched expected exception on duplicate keys: %j', 78 | e 79 | ); 80 | } 81 | ``` 82 | 83 | Output 84 | 85 | ``` 86 | Duplicate Key test with big number JSON 87 | Input: { "dupkey": "value 1", "dupkey": "value 2"} 88 | JSON.parse(dupkeys).dupkey: value 2 89 | Succesfully catched expected exception on duplicate keys: {"name":"SyntaxError","message":"Duplicate key \"dupkey\"","at":33,"text":"{ \"dupkey\": \"value 1\", \"dupkey\": \"value 2\"}"} 90 | 91 | ``` 92 | 93 | #### options.storeAsString, boolean, default false 94 | 95 | Specifies if BigInts should be stored in the object as a string, rather than the default BigNumber. 96 | 97 | Note that this is a dangerous behavior as it breaks the default functionality of being able to convert back-and-forth without data type changes (as this will convert all BigInts to be-and-stay strings). 98 | 99 | example: 100 | 101 | ```js 102 | var JSONbig = require('json-bigint'); 103 | var JSONbigString = require('json-bigint')({ storeAsString: true }); 104 | var key = '{ "key": 1234567890123456789 }'; 105 | console.log('\n\nStoring the BigInt as a string, instead of a BigNumber'); 106 | console.log('Input:', key); 107 | var withInt = JSONbig.parse(key); 108 | var withString = JSONbigString.parse(key); 109 | console.log( 110 | 'Default type: %s, With option type: %s', 111 | typeof withInt.key, 112 | typeof withString.key 113 | ); 114 | ``` 115 | 116 | Output 117 | 118 | ``` 119 | Storing the BigInt as a string, instead of a BigNumber 120 | Input: { "key": 1234567890123456789 } 121 | Default type: object, With option type: string 122 | 123 | ``` 124 | 125 | #### options.useNativeBigInt, boolean, default false 126 | 127 | Specifies if parser uses native BigInt instead of bignumber.js 128 | 129 | example: 130 | 131 | ```js 132 | var JSONbig = require('json-bigint'); 133 | var JSONbigNative = require('json-bigint')({ useNativeBigInt: true }); 134 | var key = '{ "key": 993143214321423154315154321 }'; 135 | console.log(`\n\nStoring the Number as native BigInt, instead of a BigNumber`); 136 | console.log('Input:', key); 137 | var normal = JSONbig.parse(key); 138 | var nativeBigInt = JSONbigNative.parse(key); 139 | console.log( 140 | 'Default type: %s, With option type: %s', 141 | typeof normal.key, 142 | typeof nativeBigInt.key 143 | ); 144 | ``` 145 | 146 | Output 147 | 148 | ``` 149 | Storing the Number as native BigInt, instead of a BigNumber 150 | Input: { "key": 993143214321423154315154321 } 151 | Default type: object, With option type: bigint 152 | 153 | ``` 154 | 155 | #### options.alwaysParseAsBig, boolean, default false 156 | 157 | Specifies if all numbers should be stored as BigNumber. 158 | 159 | Note that this is a dangerous behavior as it breaks the default functionality of being able to convert back-and-forth without data type changes (as this will convert all Number to be-and-stay BigNumber) 160 | 161 | example: 162 | 163 | ```js 164 | var JSONbig = require('json-bigint'); 165 | var JSONbigAlways = require('json-bigint')({ alwaysParseAsBig: true }); 166 | var key = '{ "key": 123 }'; // there is no need for BigNumber by default, but we're forcing it 167 | console.log(`\n\nStoring the Number as a BigNumber, instead of a Number`); 168 | console.log('Input:', key); 169 | var normal = JSONbig.parse(key); 170 | var always = JSONbigAlways.parse(key); 171 | console.log( 172 | 'Default type: %s, With option type: %s', 173 | typeof normal.key, 174 | typeof always.key 175 | ); 176 | ``` 177 | 178 | Output 179 | 180 | ``` 181 | Storing the Number as a BigNumber, instead of a Number 182 | Input: { "key": 123 } 183 | Default type: number, With option type: object 184 | 185 | ``` 186 | 187 | If you want to force all numbers to be parsed as native `BigInt` 188 | (you probably do! Otherwise any calulations become a real headache): 189 | 190 | ```js 191 | var JSONbig = require('json-bigint')({ 192 | alwaysParseAsBig: true, 193 | useNativeBigInt: true, 194 | }); 195 | ``` 196 | 197 | #### options.protoAction, boolean, default: "error". Possible values: "error", "ignore", "preserve" 198 | 199 | #### options.constructorAction, boolean, default: "error". Possible values: "error", "ignore", "preserve" 200 | 201 | Controls how `__proto__` and `constructor` properties are treated. If set to "error" they are not allowed and 202 | parse() call will throw an error. If set to "ignore" the prroperty and it;s value is skipped from parsing and object building. 203 | If set to "preserve" the `__proto__` property is set. One should be extra careful and make sure any other library consuming generated data 204 | is not vulnerable to prototype poisoning attacks. 205 | 206 | example: 207 | 208 | ```js 209 | var JSONbigAlways = require('json-bigint')({ protoAction: 'ignore' }); 210 | const user = JSONbig.parse('{ "__proto__": { "admin": true }, "id": 12345 }'); 211 | // => result is { id: 12345 } 212 | ``` 213 | 214 | ### Links: 215 | 216 | - [RFC4627: The application/json Media Type for JavaScript Object Notation (JSON)](http://www.ietf.org/rfc/rfc4627.txt) 217 | - [Re: \[Json\] Limitations on number size?](http://www.ietf.org/mail-archive/web/json/current/msg00297.html) 218 | - [Is there any proper way to parse JSON with large numbers? (long, bigint, int64)](http://stackoverflow.com/questions/18755125/node-js-is-there-any-proper-way-to-parse-json-with-large-numbers-long-bigint) 219 | - [What is JavaScript's Max Int? What's the highest Integer value a Number can go to without losing precision?](http://stackoverflow.com/questions/307179/what-is-javascripts-max-int-whats-the-highest-integer-value-a-number-can-go-t) 220 | - [Large numbers erroneously rounded in Javascript](http://stackoverflow.com/questions/1379934/large-numbers-erroneously-rounded-in-javascript) 221 | 222 | ### Note on native BigInt support 223 | 224 | #### Stringifying 225 | 226 | Full support out-of-the-box, stringifies BigInts as pure numbers (no quotes, no `n`) 227 | 228 | #### Limitations 229 | 230 | - Roundtrip operations 231 | 232 | `s === JSONbig.stringify(JSONbig.parse(s))` but 233 | 234 | `o !== JSONbig.parse(JSONbig.stringify(o))` 235 | 236 | when `o` has a value with something like `123n`. 237 | 238 | `JSONbig` stringify `123n` as `123`, which becomes `number` (aka `123` not `123n`) by default when being reparsed. 239 | 240 | There is currently no consistent way to deal with this issue, so we decided to leave it, handling this specific case is then up to users. 241 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | var BigNumber = null; 2 | 3 | // regexpxs extracted from 4 | // (c) BSD-3-Clause 5 | // https://github.com/fastify/secure-json-parse/graphs/contributors and https://github.com/hapijs/bourne/graphs/contributors 6 | 7 | const suspectProtoRx = /(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])/; 8 | const suspectConstructorRx = /(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)/; 9 | 10 | /* 11 | json_parse.js 12 | 2012-06-20 13 | 14 | Public Domain. 15 | 16 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 17 | 18 | This file creates a json_parse function. 19 | During create you can (optionally) specify some behavioural switches 20 | 21 | require('json-bigint')(options) 22 | 23 | The optional options parameter holds switches that drive certain 24 | aspects of the parsing process: 25 | * options.strict = true will warn about duplicate-key usage in the json. 26 | The default (strict = false) will silently ignore those and overwrite 27 | values for keys that are in duplicate use. 28 | 29 | The resulting function follows this signature: 30 | json_parse(text, reviver) 31 | This method parses a JSON text to produce an object or array. 32 | It can throw a SyntaxError exception. 33 | 34 | The optional reviver parameter is a function that can filter and 35 | transform the results. It receives each of the keys and values, 36 | and its return value is used instead of the original value. 37 | If it returns what it received, then the structure is not modified. 38 | If it returns undefined then the member is deleted. 39 | 40 | Example: 41 | 42 | // Parse the text. Values that look like ISO date strings will 43 | // be converted to Date objects. 44 | 45 | myData = json_parse(text, function (key, value) { 46 | var a; 47 | if (typeof value === 'string') { 48 | a = 49 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 50 | if (a) { 51 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 52 | +a[5], +a[6])); 53 | } 54 | } 55 | return value; 56 | }); 57 | 58 | This is a reference implementation. You are free to copy, modify, or 59 | redistribute. 60 | 61 | This code should be minified before deployment. 62 | See http://javascript.crockford.com/jsmin.html 63 | 64 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 65 | NOT CONTROL. 66 | */ 67 | 68 | /*members "", "\"", "\/", "\\", at, b, call, charAt, f, fromCharCode, 69 | hasOwnProperty, message, n, name, prototype, push, r, t, text 70 | */ 71 | 72 | var json_parse = function (options) { 73 | 'use strict'; 74 | 75 | // This is a function that can parse a JSON text, producing a JavaScript 76 | // data structure. It is a simple, recursive descent parser. It does not use 77 | // eval or regular expressions, so it can be used as a model for implementing 78 | // a JSON parser in other languages. 79 | 80 | // We are defining the function inside of another function to avoid creating 81 | // global variables. 82 | 83 | // Default options one can override by passing options to the parse() 84 | var _options = { 85 | strict: false, // not being strict means do not generate syntax errors for "duplicate key" 86 | storeAsString: false, // toggles whether the values should be stored as BigNumber (default) or a string 87 | alwaysParseAsBig: false, // toggles whether all numbers should be Big 88 | useNativeBigInt: false, // toggles whether to use native BigInt instead of bignumber.js 89 | protoAction: 'error', 90 | constructorAction: 'error', 91 | }; 92 | 93 | // If there are options, then use them to override the default _options 94 | if (options !== undefined && options !== null) { 95 | if (options.strict === true) { 96 | _options.strict = true; 97 | } 98 | if (options.storeAsString === true) { 99 | _options.storeAsString = true; 100 | } 101 | _options.alwaysParseAsBig = 102 | options.alwaysParseAsBig === true ? options.alwaysParseAsBig : false; 103 | _options.useNativeBigInt = 104 | options.useNativeBigInt === true ? options.useNativeBigInt : false; 105 | 106 | if (typeof options.constructorAction !== 'undefined') { 107 | if ( 108 | options.constructorAction === 'error' || 109 | options.constructorAction === 'ignore' || 110 | options.constructorAction === 'preserve' 111 | ) { 112 | _options.constructorAction = options.constructorAction; 113 | } else { 114 | throw new Error( 115 | `Incorrect value for constructorAction option, must be "error", "ignore" or undefined but passed ${options.constructorAction}` 116 | ); 117 | } 118 | } 119 | 120 | if (typeof options.protoAction !== 'undefined') { 121 | if ( 122 | options.protoAction === 'error' || 123 | options.protoAction === 'ignore' || 124 | options.protoAction === 'preserve' 125 | ) { 126 | _options.protoAction = options.protoAction; 127 | } else { 128 | throw new Error( 129 | `Incorrect value for protoAction option, must be "error", "ignore" or undefined but passed ${options.protoAction}` 130 | ); 131 | } 132 | } 133 | } 134 | 135 | var at, // The index of the current character 136 | ch, // The current character 137 | escapee = { 138 | '"': '"', 139 | '\\': '\\', 140 | '/': '/', 141 | b: '\b', 142 | f: '\f', 143 | n: '\n', 144 | r: '\r', 145 | t: '\t', 146 | }, 147 | text, 148 | error = function (m) { 149 | // Call error when something is wrong. 150 | 151 | throw { 152 | name: 'SyntaxError', 153 | message: m, 154 | at: at, 155 | text: text, 156 | }; 157 | }, 158 | next = function (c) { 159 | // If a c parameter is provided, verify that it matches the current character. 160 | 161 | if (c && c !== ch) { 162 | error("Expected '" + c + "' instead of '" + ch + "'"); 163 | } 164 | 165 | // Get the next character. When there are no more characters, 166 | // return the empty string. 167 | 168 | ch = text.charAt(at); 169 | at += 1; 170 | return ch; 171 | }, 172 | number = function () { 173 | // Parse a number value. 174 | 175 | var number, 176 | string = ''; 177 | 178 | if (ch === '-') { 179 | string = '-'; 180 | next('-'); 181 | } 182 | while (ch >= '0' && ch <= '9') { 183 | string += ch; 184 | next(); 185 | } 186 | if (ch === '.') { 187 | string += '.'; 188 | while (next() && ch >= '0' && ch <= '9') { 189 | string += ch; 190 | } 191 | } 192 | if (ch === 'e' || ch === 'E') { 193 | string += ch; 194 | next(); 195 | if (ch === '-' || ch === '+') { 196 | string += ch; 197 | next(); 198 | } 199 | while (ch >= '0' && ch <= '9') { 200 | string += ch; 201 | next(); 202 | } 203 | } 204 | number = +string; 205 | if (!isFinite(number)) { 206 | error('Bad number'); 207 | } else { 208 | if (BigNumber == null && !_options.useNativeBigInt) BigNumber = require('bignumber.js'); 209 | if (Number.isSafeInteger(number)) 210 | return !_options.alwaysParseAsBig 211 | ? number 212 | : _options.useNativeBigInt 213 | ? BigInt(number) 214 | : new BigNumber(number); 215 | else 216 | // Number with fractional part should be treated as number(double) including big integers in scientific notation, i.e 1.79e+308 217 | return _options.storeAsString 218 | ? string 219 | : /[\.eE]/.test(string) 220 | ? number 221 | : _options.useNativeBigInt 222 | ? BigInt(string) 223 | : new BigNumber(string); 224 | } 225 | }, 226 | string = function () { 227 | // Parse a string value. 228 | 229 | var hex, 230 | i, 231 | string = '', 232 | uffff; 233 | 234 | // When parsing for string values, we must look for " and \ characters. 235 | 236 | if (ch === '"') { 237 | var startAt = at; 238 | while (next()) { 239 | if (ch === '"') { 240 | if (at - 1 > startAt) string += text.substring(startAt, at - 1); 241 | next(); 242 | return string; 243 | } 244 | if (ch === '\\') { 245 | if (at - 1 > startAt) string += text.substring(startAt, at - 1); 246 | next(); 247 | if (ch === 'u') { 248 | uffff = 0; 249 | for (i = 0; i < 4; i += 1) { 250 | hex = parseInt(next(), 16); 251 | if (!isFinite(hex)) { 252 | break; 253 | } 254 | uffff = uffff * 16 + hex; 255 | } 256 | string += String.fromCharCode(uffff); 257 | } else if (typeof escapee[ch] === 'string') { 258 | string += escapee[ch]; 259 | } else { 260 | break; 261 | } 262 | startAt = at; 263 | } 264 | } 265 | } 266 | error('Bad string'); 267 | }, 268 | white = function () { 269 | // Skip whitespace. 270 | 271 | while (ch && ch <= ' ') { 272 | next(); 273 | } 274 | }, 275 | word = function () { 276 | // true, false, or null. 277 | 278 | switch (ch) { 279 | case 't': 280 | next('t'); 281 | next('r'); 282 | next('u'); 283 | next('e'); 284 | return true; 285 | case 'f': 286 | next('f'); 287 | next('a'); 288 | next('l'); 289 | next('s'); 290 | next('e'); 291 | return false; 292 | case 'n': 293 | next('n'); 294 | next('u'); 295 | next('l'); 296 | next('l'); 297 | return null; 298 | } 299 | error("Unexpected '" + ch + "'"); 300 | }, 301 | value, // Place holder for the value function. 302 | array = function () { 303 | // Parse an array value. 304 | 305 | var array = []; 306 | 307 | if (ch === '[') { 308 | next('['); 309 | white(); 310 | if (ch === ']') { 311 | next(']'); 312 | return array; // empty array 313 | } 314 | while (ch) { 315 | array.push(value()); 316 | white(); 317 | if (ch === ']') { 318 | next(']'); 319 | return array; 320 | } 321 | next(','); 322 | white(); 323 | } 324 | } 325 | error('Bad array'); 326 | }, 327 | object = function () { 328 | // Parse an object value. 329 | 330 | var key, 331 | object = Object.create(null); 332 | 333 | if (ch === '{') { 334 | next('{'); 335 | white(); 336 | if (ch === '}') { 337 | next('}'); 338 | return object; // empty object 339 | } 340 | while (ch) { 341 | key = string(); 342 | white(); 343 | next(':'); 344 | if ( 345 | _options.strict === true && 346 | Object.hasOwnProperty.call(object, key) 347 | ) { 348 | error('Duplicate key "' + key + '"'); 349 | } 350 | 351 | if (suspectProtoRx.test(key) === true) { 352 | if (_options.protoAction === 'error') { 353 | error('Object contains forbidden prototype property'); 354 | } else if (_options.protoAction === 'ignore') { 355 | value(); 356 | } else { 357 | object[key] = value(); 358 | } 359 | } else if (suspectConstructorRx.test(key) === true) { 360 | if (_options.constructorAction === 'error') { 361 | error('Object contains forbidden constructor property'); 362 | } else if (_options.constructorAction === 'ignore') { 363 | value(); 364 | } else { 365 | object[key] = value(); 366 | } 367 | } else { 368 | object[key] = value(); 369 | } 370 | 371 | white(); 372 | if (ch === '}') { 373 | next('}'); 374 | return object; 375 | } 376 | next(','); 377 | white(); 378 | } 379 | } 380 | error('Bad object'); 381 | }; 382 | 383 | value = function () { 384 | // Parse a JSON value. It could be an object, an array, a string, a number, 385 | // or a word. 386 | 387 | white(); 388 | switch (ch) { 389 | case '{': 390 | return object(); 391 | case '[': 392 | return array(); 393 | case '"': 394 | return string(); 395 | case '-': 396 | return number(); 397 | default: 398 | return ch >= '0' && ch <= '9' ? number() : word(); 399 | } 400 | }; 401 | 402 | // Return the json_parse function. It will have access to all of the above 403 | // functions and variables. 404 | 405 | return function (source, reviver) { 406 | var result; 407 | 408 | text = source + ''; 409 | at = 0; 410 | ch = ' '; 411 | result = value(); 412 | white(); 413 | if (ch) { 414 | error('Syntax error'); 415 | } 416 | 417 | // If there is a reviver function, we recursively walk the new structure, 418 | // passing each name/value pair to the reviver function for possible 419 | // transformation, starting with a temporary root object that holds the result 420 | // in an empty key. If there is not a reviver function, we simply return the 421 | // result. 422 | 423 | return typeof reviver === 'function' 424 | ? (function walk(holder, key) { 425 | var k, 426 | v, 427 | value = holder[key]; 428 | if (value && typeof value === 'object') { 429 | Object.keys(value).forEach(function (k) { 430 | v = walk(value, k); 431 | if (v !== undefined) { 432 | value[k] = v; 433 | } else { 434 | delete value[k]; 435 | } 436 | }); 437 | } 438 | return reviver.call(holder, key, value); 439 | })({ '': result }, '') 440 | : result; 441 | }; 442 | }; 443 | 444 | module.exports = json_parse; 445 | -------------------------------------------------------------------------------- /lib/stringify.js: -------------------------------------------------------------------------------- 1 | var BigNumber = require('bignumber.js'); 2 | 3 | /* 4 | json2.js 5 | 2013-05-26 6 | 7 | Public Domain. 8 | 9 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 10 | 11 | See http://www.JSON.org/js.html 12 | 13 | 14 | This code should be minified before deployment. 15 | See http://javascript.crockford.com/jsmin.html 16 | 17 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 18 | NOT CONTROL. 19 | 20 | 21 | This file creates a global JSON object containing two methods: stringify 22 | and parse. 23 | 24 | JSON.stringify(value, replacer, space) 25 | value any JavaScript value, usually an object or array. 26 | 27 | replacer an optional parameter that determines how object 28 | values are stringified for objects. It can be a 29 | function or an array of strings. 30 | 31 | space an optional parameter that specifies the indentation 32 | of nested structures. If it is omitted, the text will 33 | be packed without extra whitespace. If it is a number, 34 | it will specify the number of spaces to indent at each 35 | level. If it is a string (such as '\t' or ' '), 36 | it contains the characters used to indent at each level. 37 | 38 | This method produces a JSON text from a JavaScript value. 39 | 40 | When an object value is found, if the object contains a toJSON 41 | method, its toJSON method will be called and the result will be 42 | stringified. A toJSON method does not serialize: it returns the 43 | value represented by the name/value pair that should be serialized, 44 | or undefined if nothing should be serialized. The toJSON method 45 | will be passed the key associated with the value, and this will be 46 | bound to the value 47 | 48 | For example, this would serialize Dates as ISO strings. 49 | 50 | Date.prototype.toJSON = function (key) { 51 | function f(n) { 52 | // Format integers to have at least two digits. 53 | return n < 10 ? '0' + n : n; 54 | } 55 | 56 | return this.getUTCFullYear() + '-' + 57 | f(this.getUTCMonth() + 1) + '-' + 58 | f(this.getUTCDate()) + 'T' + 59 | f(this.getUTCHours()) + ':' + 60 | f(this.getUTCMinutes()) + ':' + 61 | f(this.getUTCSeconds()) + 'Z'; 62 | }; 63 | 64 | You can provide an optional replacer method. It will be passed the 65 | key and value of each member, with this bound to the containing 66 | object. The value that is returned from your method will be 67 | serialized. If your method returns undefined, then the member will 68 | be excluded from the serialization. 69 | 70 | If the replacer parameter is an array of strings, then it will be 71 | used to select the members to be serialized. It filters the results 72 | such that only members with keys listed in the replacer array are 73 | stringified. 74 | 75 | Values that do not have JSON representations, such as undefined or 76 | functions, will not be serialized. Such values in objects will be 77 | dropped; in arrays they will be replaced with null. You can use 78 | a replacer function to replace those with JSON values. 79 | JSON.stringify(undefined) returns undefined. 80 | 81 | The optional space parameter produces a stringification of the 82 | value that is filled with line breaks and indentation to make it 83 | easier to read. 84 | 85 | If the space parameter is a non-empty string, then that string will 86 | be used for indentation. If the space parameter is a number, then 87 | the indentation will be that many spaces. 88 | 89 | Example: 90 | 91 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 92 | // text is '["e",{"pluribus":"unum"}]' 93 | 94 | 95 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 96 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 97 | 98 | text = JSON.stringify([new Date()], function (key, value) { 99 | return this[key] instanceof Date ? 100 | 'Date(' + this[key] + ')' : value; 101 | }); 102 | // text is '["Date(---current time---)"]' 103 | 104 | 105 | JSON.parse(text, reviver) 106 | This method parses a JSON text to produce an object or array. 107 | It can throw a SyntaxError exception. 108 | 109 | The optional reviver parameter is a function that can filter and 110 | transform the results. It receives each of the keys and values, 111 | and its return value is used instead of the original value. 112 | If it returns what it received, then the structure is not modified. 113 | If it returns undefined then the member is deleted. 114 | 115 | Example: 116 | 117 | // Parse the text. Values that look like ISO date strings will 118 | // be converted to Date objects. 119 | 120 | myData = JSON.parse(text, function (key, value) { 121 | var a; 122 | if (typeof value === 'string') { 123 | a = 124 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 125 | if (a) { 126 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 127 | +a[5], +a[6])); 128 | } 129 | } 130 | return value; 131 | }); 132 | 133 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 134 | var d; 135 | if (typeof value === 'string' && 136 | value.slice(0, 5) === 'Date(' && 137 | value.slice(-1) === ')') { 138 | d = new Date(value.slice(5, -1)); 139 | if (d) { 140 | return d; 141 | } 142 | } 143 | return value; 144 | }); 145 | 146 | 147 | This is a reference implementation. You are free to copy, modify, or 148 | redistribute. 149 | */ 150 | 151 | /*jslint evil: true, regexp: true */ 152 | 153 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 154 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 155 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 156 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 157 | test, toJSON, toString, valueOf 158 | */ 159 | 160 | 161 | // Create a JSON object only if one does not already exist. We create the 162 | // methods in a closure to avoid creating global variables. 163 | 164 | var JSON = module.exports; 165 | 166 | (function () { 167 | 'use strict'; 168 | 169 | function f(n) { 170 | // Format integers to have at least two digits. 171 | return n < 10 ? '0' + n : n; 172 | } 173 | 174 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 175 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 176 | gap, 177 | indent, 178 | meta = { // table of character substitutions 179 | '\b': '\\b', 180 | '\t': '\\t', 181 | '\n': '\\n', 182 | '\f': '\\f', 183 | '\r': '\\r', 184 | '"' : '\\"', 185 | '\\': '\\\\' 186 | }, 187 | rep; 188 | 189 | 190 | function quote(string) { 191 | 192 | // If the string contains no control characters, no quote characters, and no 193 | // backslash characters, then we can safely slap some quotes around it. 194 | // Otherwise we must also replace the offending characters with safe escape 195 | // sequences. 196 | 197 | escapable.lastIndex = 0; 198 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 199 | var c = meta[a]; 200 | return typeof c === 'string' 201 | ? c 202 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 203 | }) + '"' : '"' + string + '"'; 204 | } 205 | 206 | 207 | function str(key, holder) { 208 | 209 | // Produce a string from holder[key]. 210 | 211 | var i, // The loop counter. 212 | k, // The member key. 213 | v, // The member value. 214 | length, 215 | mind = gap, 216 | partial, 217 | value = holder[key], 218 | isBigNumber = value != null && (value instanceof BigNumber || BigNumber.isBigNumber(value)); 219 | 220 | // If the value has a toJSON method, call it to obtain a replacement value. 221 | 222 | if (value && typeof value === 'object' && 223 | typeof value.toJSON === 'function') { 224 | value = value.toJSON(key); 225 | } 226 | 227 | // If we were called with a replacer function, then call the replacer to 228 | // obtain a replacement value. 229 | 230 | if (typeof rep === 'function') { 231 | value = rep.call(holder, key, value); 232 | } 233 | 234 | // What happens next depends on the value's type. 235 | 236 | switch (typeof value) { 237 | case 'string': 238 | if (isBigNumber) { 239 | return value; 240 | } else { 241 | return quote(value); 242 | } 243 | 244 | case 'number': 245 | 246 | // JSON numbers must be finite. Encode non-finite numbers as null. 247 | 248 | return isFinite(value) ? String(value) : 'null'; 249 | 250 | case 'boolean': 251 | case 'null': 252 | case 'bigint': 253 | 254 | // If the value is a boolean or null, convert it to a string. Note: 255 | // typeof null does not produce 'null'. The case is included here in 256 | // the remote chance that this gets fixed someday. 257 | 258 | return String(value); 259 | 260 | // If the type is 'object', we might be dealing with an object or an array or 261 | // null. 262 | 263 | case 'object': 264 | 265 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 266 | // so watch out for that case. 267 | 268 | if (!value) { 269 | return 'null'; 270 | } 271 | 272 | // Make an array to hold the partial results of stringifying this object value. 273 | 274 | gap += indent; 275 | partial = []; 276 | 277 | // Is the value an array? 278 | 279 | if (Object.prototype.toString.apply(value) === '[object Array]') { 280 | 281 | // The value is an array. Stringify every element. Use null as a placeholder 282 | // for non-JSON values. 283 | 284 | length = value.length; 285 | for (i = 0; i < length; i += 1) { 286 | partial[i] = str(i, value) || 'null'; 287 | } 288 | 289 | // Join all of the elements together, separated with commas, and wrap them in 290 | // brackets. 291 | 292 | v = partial.length === 0 293 | ? '[]' 294 | : gap 295 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' 296 | : '[' + partial.join(',') + ']'; 297 | gap = mind; 298 | return v; 299 | } 300 | 301 | // If the replacer is an array, use it to select the members to be stringified. 302 | 303 | if (rep && typeof rep === 'object') { 304 | length = rep.length; 305 | for (i = 0; i < length; i += 1) { 306 | if (typeof rep[i] === 'string') { 307 | k = rep[i]; 308 | v = str(k, value); 309 | if (v) { 310 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 311 | } 312 | } 313 | } 314 | } else { 315 | 316 | // Otherwise, iterate through all of the keys in the object. 317 | 318 | Object.keys(value).forEach(function(k) { 319 | var v = str(k, value); 320 | if (v) { 321 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 322 | } 323 | }); 324 | } 325 | 326 | // Join all of the member texts together, separated with commas, 327 | // and wrap them in braces. 328 | 329 | v = partial.length === 0 330 | ? '{}' 331 | : gap 332 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' 333 | : '{' + partial.join(',') + '}'; 334 | gap = mind; 335 | return v; 336 | } 337 | } 338 | 339 | // If the JSON object does not yet have a stringify method, give it one. 340 | 341 | if (typeof JSON.stringify !== 'function') { 342 | JSON.stringify = function (value, replacer, space) { 343 | 344 | // The stringify method takes a value and an optional replacer, and an optional 345 | // space parameter, and returns a JSON text. The replacer can be a function 346 | // that can replace values, or an array of strings that will select the keys. 347 | // A default replacer method can be provided. Use of the space parameter can 348 | // produce text that is more easily readable. 349 | 350 | var i; 351 | gap = ''; 352 | indent = ''; 353 | 354 | // If the space parameter is a number, make an indent string containing that 355 | // many spaces. 356 | 357 | if (typeof space === 'number') { 358 | for (i = 0; i < space; i += 1) { 359 | indent += ' '; 360 | } 361 | 362 | // If the space parameter is a string, it will be used as the indent string. 363 | 364 | } else if (typeof space === 'string') { 365 | indent = space; 366 | } 367 | 368 | // If there is a replacer, it must be a function or an array. 369 | // Otherwise, throw an error. 370 | 371 | rep = replacer; 372 | if (replacer && typeof replacer !== 'function' && 373 | (typeof replacer !== 'object' || 374 | typeof replacer.length !== 'number')) { 375 | throw new Error('JSON.stringify'); 376 | } 377 | 378 | // Make a fake root object containing our value under the key of ''. 379 | // Return the result of stringifying the value. 380 | 381 | return str('', {'': value}); 382 | }; 383 | } 384 | }()); 385 | --------------------------------------------------------------------------------