lossless-json
Advanced tools
Comparing version 1.0.5 to 2.0.0
@@ -1,2 +0,5 @@ | ||
!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((r="undefined"!=typeof globalThis?globalThis:r||self).LosslessJSON={})}(this,function(r){"use strict";var e=!0;function o(r){return r&&void 0!==r.circularRefs&&null!==r.circularRefs&&(e=!0===r.circularRefs),{circularRefs:e}}function i(r){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(r){return typeof r}:function(r){return r&&"function"==typeof Symbol&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r})(r)}function f(r,e){for(var n=0;n<e.length;n++){var t=e[n];t.enumerable=t.enumerable||!1,t.configurable=!0,"value"in t&&(t.writable=!0),Object.defineProperty(r,t.key,t)}}var u=function(){function e(r){!function(r,e){if(!(r instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e),this.value=function r(e){{if("string"==typeof e){if(!l(e))throw new Error('Invalid number (value: "'+e+'")');return e}if("number"!=typeof e)return r(e&&e.valueOf());if(15<a(e+"").length)throw new Error("Invalid number: contains more than 15 digits (value: "+e+")");if(isNaN(e))throw new Error("Invalid number: NaN");if(!isFinite(e))throw new Error("Invalid number: Infinity");return e+""}}(r),this.type="LosslessNumber",this.isLosslessNumber=!0}var r,n,t;return r=e,(n=[{key:"valueOf",value:function(){var r=parseFloat(this.value),e=a(this.value);if(15<e.length)throw new Error("Cannot convert to number: number would be truncated (value: "+this.value+")");if(!isFinite(r))throw new Error("Cannot convert to number: number would overflow (value: "+this.value+")");if(Math.abs(r)<Number.MIN_VALUE&&!/^0*$/.test(e))throw new Error("Cannot convert to number: number would underflow (value: "+this.value+")");return r}},{key:"toString",value:function(){return this.value}}])&&f(r.prototype,n),t&&f(r,t),e}();function a(r){return("string"!=typeof r?r+"":r).replace(/^-/,"").replace(/e.*$/,"").replace(/^0\.?0*|\./,"")}function l(r){return/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(r)}function c(r,e,n,t){return Array.isArray(n)?t.call(r,e,function(r,e){for(var n=[],t=0;t<r.length;t++)n[t]=c(r,t+"",r[t],e);return n}(n,t)):n&&"object"===i(n)&&!n.isLosslessNumber?t.call(r,e,function(r,e){var n,t={};for(n in r)r.hasOwnProperty(n)&&(t[n]=c(r,n,r[n],e));return t}(n,t)):t.call(r,e,n)}function n(r){return encodeURIComponent(r.replace(/\//g,"~1").replace(/~/g,"~0"))}function s(r){return decodeURIComponent(r).replace(/~1/g,"/").replace(/~0/g,"~")}function h(r){return"#/"+r.map(n).join("/")}var v={NULL:0,DELIMITER:1,NUMBER:2,STRING:3,SYMBOL:4,UNKNOWN:5},t={"":!0,"{":!0,"}":!0,"[":!0,"]":!0,":":!0,",":!0},p={'"':'"',"\\":"\\","/":"/",b:"\b",f:"\f",n:"\n",r:"\r",t:"\t"},d="",y=0,b="",g="",m=v.NULL,w=[],I=[];function E(){y++,b=d.charAt(y)}function N(){for(m=v.NULL,g="";" "===b||"\t"===b||"\n"===b||"\r"===b;)E();if(t[b])return m=v.DELIMITER,g=b,void E();if(R(b)||"-"===b){if(m=v.NUMBER,"-"===b){if(g+=b,E(),!R(b))throw S("Invalid number, digit expected",y)}else"0"===b&&(g+=b,E());for(;R(b);)g+=b,E();if("."===b){if(g+=b,E(),!R(b))throw S("Invalid number, digit expected",y);for(;R(b);)g+=b,E()}if("e"===b||"E"===b){if(g+=b,E(),"+"!==b&&"-"!==b||(g+=b,E()),!R(b))throw S("Invalid number, digit expected",y);for(;R(b);)g+=b,E()}}else if('"'!==b){if(!L(b)){for(m=v.UNKNOWN;""!==b;)g+=b,E();throw S('Syntax error in part "'+g+'"')}for(m=v.SYMBOL;L(b);)g+=b,E()}else{for(m=v.STRING,E();""!==b&&'"'!==b;)if("\\"===b){E();var r=p[b];if(void 0!==r)g+=r,E();else{if("u"!==b)throw S('Invalid escape character "\\'+b+'"',y);E();for(var e="",n=0;n<4;n++){if(!/^[0-9a-fA-F]/.test(b))throw S("Invalid unicode character");e+=b,E()}g+=String.fromCharCode(parseInt(e,16))}}else g+=b,E();if('"'!==b)throw S("End of string expected");E()}}function L(r){return/^[a-zA-Z_]/.test(r)}function R(r){return"0"<=r&&r<="9"}function S(r,e){void 0===e&&(e=y-g.length);r=new SyntaxError(r+" (char "+e+")");return r.char=e,r}function O(){if(m!==v.DELIMITER||"{"!==g)return function(){if(m!==v.DELIMITER||"["!==g)return function(){if(m!==v.STRING)return function(){if(m!==v.NUMBER)return function(){if(m!==v.SYMBOL)return function(){throw S(""===g?"Unexpected end of json string":"Value expected")}();if("true"===g)return N(),!0;if("false"===g)return N(),!1;if("null"!==g)throw S('Unknown symbol "'+g+'"');return N(),null}();var r=new u(g);return N(),r}();var r=g;return N(),r}();N();var r=[];if(m===v.DELIMITER&&"]"===g)return N(),r;var e=I.length;I[e]=r;for(;w[e]=r.length+"",r.push(O()),m===v.DELIMITER&&","===g;)N();if(m===v.DELIMITER&&"]"===g)return N(),I.length=e,w.length=e,r;throw S('Comma or end of array "]" expected')}();var r,e;N();var n={};if(m===v.DELIMITER&&"}"===g)return N(),n;var t=I.length;for(I[t]=n;;){if(m!==v.STRING)throw S("Object key expected");if(e=g,N(),m!==v.DELIMITER||":"!==g)throw S("Colon expected");if(N(),n[w[t]=e]=O(),m!==v.DELIMITER||","!==g)break;N()}if(m!==v.DELIMITER||"}"!==g)throw S('Comma or end of object "}" expected');return N(),"string"==typeof(r=n).$ref&&1===Object.keys(r).length?function(r){if(!o().circularRefs)return r;for(var e=function(r){if("#"!==(r=r.split("/").map(s)).shift())throw SyntaxError("Cannot parse JSON Pointer: no valid URI fragment");return""===r[r.length-1]&&r.pop(),r}(r.$ref),n=0;n<e.length;n++)if(e[n]!==w[n])throw new Error('Invalid circular reference "'+r.$ref+'"');return I[e.length]}(n):(I.length=t,w.length=t,n)}var M=[],x=[];function T(r,e,n){x=[],M=[];var t,r="function"==typeof e?e.call({"":r},"",r):r;return"number"==typeof n?10<n?t=j(" ",10):1<=n&&(t=j(" ",n)):"string"==typeof n&&""!==n&&(t=n),U(r,e,t,"")}function U(r,e,n,t){return"boolean"==typeof r||r instanceof Boolean||null===r||"number"==typeof r||r instanceof Number||"string"==typeof r||r instanceof String||r instanceof Date?JSON.stringify(r):r&&r.isLosslessNumber?r.value:Array.isArray(r)?function(r,e,n,t){var o=n?t+n:void 0,i=n?"[\n":"[";if(D(r))return A(r,e,n,t);var f=x.length;x[f]=r;for(var u=0;u<r.length;u++){var a=u+"",l="function"==typeof e?e.call(r,a,r[u]):r[u];n&&(i+=o),void 0!==l&&"function"!=typeof l?(M[f]=a,i+=U(l,e,n,o)):i+="null",u<r.length-1&&(i+=n?",\n":",")}return x.length=f,M.length=f,i+=n?"\n"+t+"]":"]"}(r,e,n,t):r&&"object"===i(r)?C(r,e,n,t):void 0}function C(r,e,n,t){var o=n?t+n:void 0,i=!0,f=n?"{\n":"{";if("function"==typeof r.toJSON)return T(r.toJSON(),e,n);if(D(r))return A(r,e,n,t);var u,a,l,c,s,h=x.length;for(u in x[h]=r)r.hasOwnProperty(u)&&(a="function"==typeof e?e.call(r,u,r[u]):r[u],l=u,s=e,void 0===(c=a)||"function"==typeof c||Array.isArray(s)&&!function(r,e){for(var n=0;n<r.length;n++)if(r[n]==e)return!0;return!1}(s,l)||(i?i=!1:f+=n?",\n":",",l=JSON.stringify(u),f+=n?o+l+": ":l+":",M[h]=u,f+=U(a,e,n,o)));return x.length=h,M.length=h,f+=n?"\n"+t+"}":"}"}function D(r){return-1!==x.indexOf(r)}function A(r,e,n,t){if(!o().circularRefs)throw new Error('Circular reference at "'+h(M)+'"');r=x.indexOf(r);return C({$ref:h(M.slice(0,r))},e,n,t)}function j(r,e){for(var n="";0<e--;)n+=r;return n}r.LosslessNumber=u,r.config=o,r.parse=function(r,e){y=0,b=(d=r).charAt(0),g="",m=v.NULL,I=[],w=[],N();var n=O();if(""!==g)throw S("Unexpected characters");return e?c({"":r=n},"",r,e):n},r.stringify=T,Object.defineProperty(r,"__esModule",{value:!0})}); | ||
//# sourceMappingURL=lossless-json.js.map | ||
// TODO: deprecated since v2, remove this deprecation warning some day | ||
throw new Error( | ||
'The bundled file "lossless-json/dist/lossless-json.js" has removed since lossless-json@2.0.0. ' + | ||
'Please use the UMD bundle "lossless-json/lib/umd/lossless-json.js" instead.' | ||
) |
# History | ||
## 2022-09-28, version 2.0.0 | ||
**IMPORTANT: BREAKING CHANGES** | ||
Breaking changes: | ||
- Function `parse` now throws an error when a duplicate key is encountered. | ||
- Dropped support for circular references. If you encounter circular references in your data structures, please rethink your datastructures: better prevent circular references in the first place. | ||
- The constructor of the `LosslessNumber` class now only supports a string as argument. Use `toLosslessNumber` to convert a number into a LosslessNumber in a safe way. | ||
- Dropped the undocumented property `.type` on `LosslessNumber` instances. Please use `.isLosslessNumber` instead. | ||
- Dropped official support for Node.js 12. | ||
Non-breaking changes: | ||
- Serialization of numeric values is now fully customizable via new options `parseNumber` and `numberStringifiers`, making it easier to integrate with a BigNumber library, or to write your own logic to turn numeric values into `bigint` when needed. | ||
- Built in support for `bigint`. | ||
- Built-in support for `Date` (turned off by default), see `reviveDate`. | ||
- Export a set of utility functions: `isInteger`, `isNumber`, `isSafeNumber`, `toSafeNumberOrThrow`, `getUnsafeNumberReason`, `parseLosslessNumber`, `parseNumberAndBigInt`, `reviveDate`, `isLosslessNumber`, `toLosslessNumber`. | ||
- THe library is modular now: it exports ES modules and an UMD bundle. The ES modules allow to import only the functions that you need, instead of pulling in the full bundle. | ||
- The library now comes with TypeScript definitions, and the code has been rewritten in TypeScript, | ||
- Performance of both `parse` and `stringify` has been improved a lot. | ||
## 2021-07-22, version 1.0.5 | ||
- Fixed stringifing of object keys containing special characters like backslash, | ||
see #239. Thanks @mengfanliao. | ||
- Fixed stringifing of object keys containing special characters like backslash, see #239. Thanks @mengfanliao. | ||
## 2020-05-08, version 1.0.4 | ||
@@ -14,9 +33,6 @@ | ||
## 2018-07-31, version 1.0.3 | ||
- Improved performance of `stringify` by using `JSON.stringify` where | ||
possible. Thanks @SergeyFromHell for the suggestion (see #5). | ||
- Improved performance of `stringify` by using `JSON.stringify` where possible. Thanks @SergeyFromHell for the suggestion (see #5). | ||
## 2018-02-11, version 1.0.2 | ||
@@ -27,3 +43,2 @@ | ||
## 2017-10-17, version 1.0.1 | ||
@@ -33,3 +48,2 @@ | ||
## 2016-02-13, version 1.0.0 | ||
@@ -39,3 +53,2 @@ | ||
## 2016-02-12, version 0.1.0 | ||
@@ -47,13 +60,10 @@ | ||
## 2016-02-08, version 0.0.2 | ||
- The `LosslessNumber` class now throws errors when you would lose information | ||
when converting from and to a `LosslessNumber`. | ||
- The `LosslessNumber` class now throws errors when you would lose information when converting from and to a `LosslessNumber`. | ||
- Handle escape characters when stringifying a string. | ||
- Exposed `LosslessNumber` in public API. | ||
## 2016-02-08, version 0.0.1 | ||
- First functional version which can parse and stringify. |
The MIT License (MIT) | ||
Copyright (c) 2016-2021 Jos de Jong | ||
Copyright (c) 2016-2022 Jos de Jong | ||
@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
{ | ||
"name": "lossless-json", | ||
"version": "1.0.5", | ||
"version": "2.0.0", | ||
"description": "Parse JSON without risk of losing numeric information", | ||
"main": "./dist/lossless-json.js", | ||
"type": "module", | ||
"main": "lib/esm/index.js", | ||
"module": "lib/esm/index.js", | ||
"browser": "lib/umd/lossless-json.js", | ||
"types": "lib/types/index.d.ts", | ||
"sideEffects": false, | ||
"repository": { | ||
@@ -11,6 +16,14 @@ "type": "git", | ||
"scripts": { | ||
"test": "ava **/*.test.js", | ||
"build": "rollup --config rollup.config.js", | ||
"test": "jest --rootDir=test", | ||
"test:lib": "npm run build && jest --rootDir='test-lib'", | ||
"build": "npm-run-all build:**", | ||
"build:clean": "del-cli lib", | ||
"build:esm": "babel src --out-dir lib/esm --extensions \".ts\" --source-maps --config-file ./babel.config.json", | ||
"build:umd": "rollup --config rollup.config.js && cpy tools/cjs/package.json lib/umd --flat", | ||
"build:types": "tsc --project tsconfig-types.json", | ||
"lint": "prettier --ignore-path .gitignore lib --check . && eslint src/**/*.ts test/**/*.ts test-lib/**/*.mjs tools/**/*.mjs", | ||
"format": "prettier --ignore-path .gitignore lib --write . && npm run lint -- --fix", | ||
"build-and-test": "npm run build && jest && npm run lint", | ||
"prepublishOnly": "npm test && npm run build", | ||
"benchmark": "babel-node test/benchmark/run.js" | ||
"benchmark": "npm run build:esm && node tools/benchmark/run.mjs" | ||
}, | ||
@@ -20,8 +33,10 @@ "keywords": [ | ||
"lossless", | ||
"circular", | ||
"parse", | ||
"stringify", | ||
"long", | ||
"bigint", | ||
"bignumber", | ||
"number", | ||
"long", | ||
"bignumber" | ||
"date", | ||
"safe" | ||
], | ||
@@ -31,21 +46,36 @@ "author": "Jos de Jong", | ||
"devDependencies": { | ||
"@babel/core": "7.14.8", | ||
"@babel/preset-env": "7.14.8", | ||
"@babel/register": "7.14.5", | ||
"ava": "3.15.0", | ||
"rollup": "2.53.3", | ||
"rollup-plugin-babel": "4.4.0", | ||
"rollup-plugin-uglify": "6.0.4" | ||
"@babel/cli": "7.19.3", | ||
"@babel/core": "7.19.3", | ||
"@babel/plugin-transform-typescript": "7.19.3", | ||
"@babel/preset-env": "7.19.3", | ||
"@babel/preset-typescript": "7.18.6", | ||
"@types/benchmark": "2.1.2", | ||
"@types/jest": "29.0.3", | ||
"@typescript-eslint/eslint-plugin": "5.38.1", | ||
"@typescript-eslint/parser": "5.38.1", | ||
"benchmark": "2.1.4", | ||
"cpy-cli": "4.2.0", | ||
"decimal.js": "10.4.1", | ||
"del-cli": "5.0.0", | ||
"eslint": "8.24.0", | ||
"eslint-config-standard": "17.0.0", | ||
"eslint-plugin-import": "2.26.0", | ||
"eslint-plugin-n": "15.3.0", | ||
"eslint-plugin-node": "11.1.0", | ||
"eslint-plugin-promise": "6.0.1", | ||
"jest": "29.1.1", | ||
"npm-run-all": "4.1.5", | ||
"prettier": "2.7.1", | ||
"rollup": "2.79.1", | ||
"rollup-plugin-terser": "7.0.2", | ||
"ts-jest": "29.0.2", | ||
"typescript": "4.8.4" | ||
}, | ||
"files": [ | ||
"dist", | ||
"lib", | ||
"HISTORY.md", | ||
"LICENSE.md", | ||
"README.md" | ||
], | ||
"ava": { | ||
"require": [ | ||
"@babel/register" | ||
] | ||
} | ||
] | ||
} |
419
README.md
@@ -6,26 +6,34 @@ # lossless-json | ||
```js | ||
let text = '{"normal":2.3,"long":123456789012345678901,"big":2.3e+500}'; | ||
import { parse, stringify } from 'lossless-json' | ||
const text = '{"decimal":2.370,"long":9223372036854775827,"big":2.3e+500}' | ||
// JSON.parse will lose some digits and a whole number: | ||
console.log(JSON.stringify(JSON.parse(text))); | ||
// '{"normal":2.3,"long":123456789012345680000,"big":null}' WHOOPS!!! | ||
console.log(JSON.stringify(JSON.parse(text))) | ||
// '{"decimal":2.37,"long":9223372036854776000,"big":null}' | ||
// WHOOPS!!! | ||
// LosslessJSON.parse will preserve big numbers: | ||
console.log(LosslessJSON.stringify(LosslessJSON.parse(text))); | ||
// '{"normal":2.3,"long":123456789012345678901,"big":2.3e+500}' | ||
// LosslessJSON.parse will preserve all numbers and even the formatting: | ||
console.log(stringify(parse(text))) | ||
// '{"decimal":2.370,"long":9223372036854775827,"big":2.3e+500}' | ||
``` | ||
**How does it work?** The library works exactly the same as the native `JSON.parse` and `JSON.stringify`. The difference is that `lossless-json` preserves information of big numbers. `lossless-json` parses numeric values not as a regular number but as a `LosslessNumber`, a data type which stores the numeric value as a string. One can perform regular operations with a `LosslessNumber`, and it will throw an error when this would result in losing information. | ||
**How does it work?** The library works exactly the same as the native `JSON.parse` and `JSON.stringify`. The difference is that `lossless-json` preserves information of big numbers. `lossless-json` parses numeric values not as a regular number but as a `LosslessNumber`, a lightweight class which stores the numeric value as a string. One can perform regular operations with a `LosslessNumber`, and it will throw an error when this would result in losing information. | ||
**When to use?** Only in some special cases. For example when you have to create some sort of data processing middleware which has to process arbitrary JSON without risk of screwing up. JSON objects containing big numbers are rare in the wild. It can occur for example when interoperating with applications written in C++, Java, or C#, which support data types like `long`. Parsing a `long` into a JavaScript `number` can result in losing information because a `long` can hold more digits than a `number`. If possible, it's preferable to change these applications such that they serialize big numbers in a safer way, for example in a stringified form. If that's not feasible, `lossless-json` is here to help you out. | ||
**When to use?** If you have to deal with JSON data that contains `long` values for example, coming from an application like C++, Java, or C#. The trade-off is that `lossless-json` is slower than the native `JSON.parse` and `JSON.stringify` functions, so be careful when performance is a bottleneck for you. | ||
Features: | ||
- No risk of losing numeric information when parsing JSON containing big numbers. | ||
- Supports circular references. | ||
- Compatible with the native `JSON.parse` and `JSON.stringify`. | ||
- No risk of losing numeric information when working with big numbers. | ||
- Parse error on duplicate keys. | ||
- Built-in support for `bigint`. | ||
- Built-in support for `Date` (turned off by default). | ||
- Customizable: parse numeric values into any data type, like `BigNumber`, `bigint`, `number`, or a mix of them. | ||
- Compatible with the native, built-in `JSON.parse` and `JSON.stringify`. | ||
- Helpful error messages when parsing invalid JSON. | ||
- Works in browsers and node.js. | ||
- Less then 3kB when minified and gzipped. | ||
- Comes with TypeScript typings included. | ||
- Modular: ES module functions, only load and bundle what you use. | ||
- The full bundle is less than 4kB in size when minified and gzipped. | ||
## Install | ||
@@ -39,3 +47,2 @@ | ||
## Use | ||
@@ -48,7 +55,6 @@ | ||
```js | ||
'use strict'; | ||
const LosslessJSON = require('lossless-json'); | ||
import { parse, stringify } from 'lossless-json' | ||
let json = LosslessJSON.parse('{"foo":"bar"}'); // {foo: 'bar'} | ||
let text = LosslessJSON.stringify(json); // '{"foo":"bar"}' | ||
const json = parse('{"foo":"bar"}') // {foo: 'bar'} | ||
const text = stringify(json) // '{"foo":"bar"}' | ||
``` | ||
@@ -61,153 +67,149 @@ | ||
```js | ||
'use strict'; | ||
const LosslessJSON = require('lossless-json'); | ||
import { parse } from 'lossless-json' | ||
let text = '{"normal":2.3,"long":123456789012345678901,"big":2.3e+500}'; | ||
let json = LosslessJSON.parse(text); | ||
const text = '{"normal":2.3,"long":123456789012345678901,"big":2.3e+500}' | ||
const json = parse(text) | ||
console.log(json.normal.isLosslessNumber); // true | ||
console.log(json.normal.valueOf()); // number, 2.3 | ||
console.log(json.normal + 2); // number, 4.3 | ||
console.log(json.normal.isLosslessNumber) // true | ||
console.log(json.normal.valueOf()) // number, 2.3 | ||
// the following operations will throw an error | ||
// as they would result in information loss | ||
console.log(json.long + 1); // throws Error Cannot convert to number: number would be truncated | ||
console.log(json.big + 1); // throws Error Cannot convert to number: number would overflow | ||
// LosslessNumbers can be used as regular numbers | ||
console.log(json.normal + 2) // number, 4.3 | ||
// but the following operation will throw an error as it would result in information loss | ||
console.log(json.long + 1) | ||
// throws Error: Cannot safely convert LosslessNumber to number: | ||
// "123456789012345678901" will be parsed as 123456789012345680000 and lose information | ||
``` | ||
If you want parse a json string into an object with regular numbers, but want to validate that no numeric information is lost, you can parse the json string using `lossless-json` and immediately convert LosslessNumbers into numbers using a reviver: | ||
### BigInt | ||
JavaScript natively supports `bigint`: big integers that can hold a large number of digits, instead of the about 15 digits that a regular `number` can hold. It is a typical use case to want to parse integer numbers into a `bigint`, and all other values into a regular `number`. This can be achieved with a custom `numberParser`: | ||
```js | ||
'use strict'; | ||
const LosslessJSON = require('lossless-json'); | ||
import { parse, isInteger } from 'lossless-json' | ||
// convert LosslessNumber to number | ||
// will throw an error if this results in information loss | ||
function convertLosslessNumber (key, value) { | ||
if (value && value.isLosslessNumber) { | ||
return value.valueOf(); | ||
// parse integer values into a bigint, and use a regular number otherwise | ||
export function customNumberParser(value) { | ||
return isInteger(value) ? BigInt(value) : parseFloat(value) | ||
} | ||
const text = '[123456789123456789123456789, 2.3, 123]' | ||
const json = parse(text, null, customNumberParser) | ||
// output: | ||
// [ | ||
// 123456789123456789123456789n, // bigint | ||
// 2.3, // number | ||
// 123n // bigint | ||
// ] | ||
``` | ||
You can adjust the logic to your liking, using utility functions like `isInteger`, `isNumber`, `isSafeNumber`. The number parser shown above is included in the library and is named `parseNumberAndBigInt`. | ||
### Validate safe numbers | ||
If you want parse a json string into an object with regular numbers, but want to validate that no numeric information is lost, you write your own number parser and use `isSafeNumber` to validate the numbers: | ||
```js | ||
import { parse, isSafeNumber } from 'lossless-json' | ||
function parseAndValidateNumber(value) { | ||
if (!isSafeNumber(value)) { | ||
throw new Error(`Cannot safely convert value '${value}' into a number`) | ||
} | ||
else { | ||
return value; | ||
} | ||
return parseFloat(value) | ||
} | ||
// will parse with success if all values can be represented with a number | ||
let json = LosslessJSON.parse('[1,2,3]', convertLosslessNumber); | ||
console.log(json); // [1, 2, 3] (regular numbers) | ||
let json = parse('[1,2,3]', undefined, parseAndValidateNumber) | ||
console.log(json) // [1, 2, 3] (regular numbers) | ||
// will throw an error when some of the values are too large to represent correctly as number | ||
try { | ||
let json = LosslessJSON.parse('[1,2e+500,3]', convertLosslessNumber); | ||
let json = parse('[1,2e+500,3]', undefined, parseAndValidateNumber) | ||
} catch (err) { | ||
console.log(err) // throws Error 'Cannot safely convert value '2e+500' into a number' | ||
} | ||
catch (err) { | ||
console.log(err); // throws Error Cannot convert to number: number would overflow | ||
} | ||
``` | ||
### BigNumbers | ||
To use the library in conjunction with your favorite BigNumber library, for example [decimal.js](https://github.com/MikeMcl/decimal.js/), you can use a replacer and reviver: | ||
To use the library in conjunction with your favorite BigNumber library, for example [decimal.js](https://github.com/MikeMcl/decimal.js/). You have to define a custom number parser and stringifier: | ||
```js | ||
'use strict'; | ||
const LosslessJSON = require('lossless-json'); | ||
const Decimal = require('decimal.js'); | ||
import { parse, stringify } from 'lossless-json' | ||
import Decimal from 'decimal.js' | ||
// convert LosslessNumber to Decimal | ||
function reviver (key, value) { | ||
if (value && value.isLosslessNumber) { | ||
return new Decimal(value.toString()); | ||
} | ||
else { | ||
return value; | ||
} | ||
} | ||
const parseDecimal = (value) => new Decimal(value) | ||
// convert Decimal to LosslessNumber | ||
function replacer (key, value) { | ||
if (value instanceof Decimal) { | ||
return new LosslessJSON.LosslessNumber(value.toString()); | ||
} | ||
else { | ||
return value; | ||
} | ||
const decimalStringifier = { | ||
test: (value) => Decimal.isDecimal(value), | ||
stringify: (value) => value.toString() | ||
} | ||
// parse JSON, operate on a Decimal value, then stringify again | ||
let text = '{"value":2.3e500}'; | ||
let json = LosslessJSON.parse(text, reviver); // {value: new Decimal('2.3e500')} | ||
let result = { // {result: new Decimal('4.6e500')} | ||
const text = '{"value":2.3e500}' | ||
const json = parse(text, undefined, parseDecimal) // {value: new Decimal('2.3e500')} | ||
const output = { | ||
// {result: new Decimal('4.6e500')} | ||
result: json.value.times(2) | ||
}; | ||
let str = LosslessJSON.stringify(json, replacer); // '{"result":4.6e500}' | ||
} | ||
const str = stringify(output, undefined, undefined, [decimalStringifier]) | ||
// '{"result":4.6e500}' | ||
``` | ||
### Circular references | ||
### Reviver and replacer | ||
`lossless-json` automatically stringifies and restores circular references. Circular references are encoded as a [JSON Pointer URI fragment](https://tools.ietf.org/html/rfc6901#section-6). | ||
The library is compatible with the native `JSON.parse` and `JSON.stringify`, and also comes with the optional `reviver` and `replacer` arguments that allow you to serialize for example data classes in a custom way. Here is an example demonstrating how you can stringify a `Date` as an object with a `$date` key instead of a string, so it is uniquely recognizable when parsing the structure: | ||
```js | ||
'use strict'; | ||
const LosslessJSON = require('lossless-json'); | ||
import { parse, stringify } from 'lossless-json' | ||
// create an object containing a circular reference to `foo` inside `bar` | ||
let json = { | ||
foo: { | ||
bar: {} | ||
// stringify a Date as a unique object with a key '$date', so it is recognizable | ||
function dateReplacer(key, value) { | ||
if (value instanceof Date) { | ||
return { | ||
$date: value.toISOString() | ||
} | ||
} | ||
}; | ||
json.foo.bar.foo = json.foo; | ||
let text = LosslessJSON.stringify(json); | ||
// text = '"{"foo":{"bar":{"foo":{"$ref":"#/foo"}}}}"' | ||
``` | ||
return value | ||
} | ||
When resolving circular references is not desirable, resolving circular references can be turned off: | ||
function isJSONDateObject(value) { | ||
return value && typeof value === 'object' && typeof value.$date === 'string' | ||
} | ||
```js | ||
'use strict'; | ||
const LosslessJSON = require('lossless-json'); | ||
// disable circular references | ||
LosslessJSON.config({circularRefs: false}); | ||
// create an object containing a circular reference to `foo` inside `bar` | ||
let json = { | ||
foo: { | ||
bar: {} | ||
function dateReviver(key, value) { | ||
if (isJSONDateObject(value)) { | ||
return new Date(value.$date) | ||
} | ||
}; | ||
json.foo.bar.foo = json.foo; | ||
try { | ||
let text = LosslessJSON.stringify(json); | ||
return value | ||
} | ||
catch (err) { | ||
console.log(err); // Error: Circular reference at "#/foo/bar/foo" | ||
const record = { | ||
message: 'Hello World', | ||
timestamp: new Date('2022-08-30T09:00:00Z') | ||
} | ||
const text = stringify(record, dateReplacer) | ||
console.log(text) | ||
// output: | ||
// '{"message":"Hello World","timestamp":{"$date":"2022-08-30T09:00:00.000Z"}}' | ||
const parsed = parse(text, dateReviver) | ||
console.log(parsed) | ||
// output: | ||
// { | ||
// action: 'create', | ||
// timestamp: new Date('2022-08-30T09:00:00.000Z') | ||
// } | ||
``` | ||
## API | ||
### parse(text [, reviver [, parseNumber]]) | ||
### config([options]) | ||
Get and/or set configuration options for `lossless-json`. | ||
- **@param** `[{circularRefs: boolean}] [options]` | ||
Optional new configuration to be applied. | ||
- **@returns** `{{circularRefs}}` | ||
Returns an object with the current configuration. | ||
The following options are available: | ||
- `{boolean} circularRefs : true` | ||
When `true` (default), `LosslessJSON.stringify` will resolve circular references. When `false`, the function will throw an error when a circular reference is encountered. | ||
### parse(text [, reviver]) | ||
The `LosslessJSON.parse()` function parses a string as JSON, optionally transforming the value produced by parsing. | ||
@@ -217,33 +219,25 @@ | ||
The string to parse as JSON. See the JSON object for a description of JSON syntax. | ||
- **@param** `{function(key: string, value: *)} [reviver]` | ||
If a function, prescribes how the value originally produced by parsing is | ||
* transformed, before being returned. | ||
- **@returns** `{*}` | ||
- **@param** `{(key: string, value: JSONValue) => JavaScriptValue} [reviver]` | ||
If a function, prescribes how the value originally produced by parsing is transformed, before being returned. | ||
- **@param** `{function(value: string) : JavaScriptValue} [parseNumber]` | ||
Pass an optional custom number parser. Input is a string, and the output can be any numeric value: `number`, `bigint`, `LosslessNumber`, or a custom `BigNumber` library. By default, all numeric values are parsed into a `LosslessNumber`. | ||
- **@returns** `{JavaScriptValue}` | ||
Returns the Object corresponding to the given JSON text. | ||
- **@throws** Throws a SyntaxError exception if the string to parse is not valid JSON. | ||
### stringify(value [, replacer [, space]]) | ||
### stringify(value [, replacer [, space [, numberStringifiers]]]) | ||
The `LosslessJSON.stringify()`` function converts a JavaScript value to a JSON string, | ||
optionally replacing values if a replacer function is specified, or | ||
optionally including only the specified properties if a replacer array is specified. | ||
The `LosslessJSON.stringify()` function converts a JavaScript value to a JSON string, optionally replacing values if a replacer function is specified, or optionally including only the specified properties if a replacer array is specified. | ||
- **@param** `{*} value` | ||
- **@param** `{JavaScriptValue} value` | ||
The value to convert to a JSON string. | ||
- **@param** `{function(key: string, value: *) | Array.<string | number>} [replacer]` | ||
A function that alters the behavior of the stringification process, | ||
or an array of String and Number objects that serve as a whitelist for | ||
selecting the properties of the value object to be included in the JSON string. | ||
If this value is null or not provided, all properties of the object are | ||
included in the resulting JSON string. | ||
- **@param** `{number | string} [space]` | ||
A String or Number object that's used to insert white space into the output | ||
JSON string for readability purposes. If this is a Number, it indicates the | ||
number of space characters to use as white space; this number is capped at 10 | ||
if it's larger than that. Values less than 1 indicate that no space should be | ||
used. If this is a String, the string (or the first 10 characters of the string, | ||
if it's longer than that) is used as white space. If this parameter is not | ||
provided (or is null), no white space is used. | ||
- **@param** `{((key: string, value: JavaScriptValue) => JSONValue) | Array.<string | number>} [replacer]` | ||
A function that alters the behavior of the stringification process, or an array with strings or numbers that serve as a whitelist for selecting the properties of the value object to be included in the JSON string. If this value is `null` or not provided, all properties of the object are included in the resulting JSON string. | ||
- **@param** `{number | string | undefined} [space]` | ||
A `string` or `number` that is used to insert white space into the output JSON string for readability purposes. If this is a `number`, it indicates the number of space characters to use as white space. Values less than 1 indicate that no space should be used. If this is a `string`, the `string` is used as white space. If this parameter is not provided (or is `null`), no white space is used. | ||
- **@param** `{Array<{test: (value: JavaScriptValue) => boolean, stringify: (value: JavaScriptValue) => string}>} [numberStringifiers]` | ||
An optional list with additional number stringifiers, for example to serialize a `BigNumber`. The output of the function must be valid stringified JSON number. When `undefined` is returned, the property will be deleted from the object. The difference with using a `replacer` is that the output of a `replacer` must be JSON and will be stringified afterwards, whereas the output of the `numberStringifiers` is already stringified JSON. | ||
- **@returns** `{string | undefined}` | ||
Returns the string representation of the JSON object. | ||
- **@throws** Throws a SyntaxError when one of the `numberStringifiers` does not return valid output. | ||
@@ -255,3 +249,3 @@ ### LosslessNumber | ||
``` | ||
new LosslessJSON.LosslessNumber(value: number | string) : LosslessNumber | ||
new LosslessNumber(value: number | string) : LosslessNumber | ||
``` | ||
@@ -261,16 +255,100 @@ | ||
- `valueOf() : number` | ||
Convert the LosslessNumber to a regular number. | ||
Throws an Error when this would result in loss of information: when the numbers digits would be truncated, or when the number would overflow or underflow. | ||
- `toString() : string` | ||
- `.valueOf(): number | bigint` | ||
Convert the `LosslessNumber` into a regular `number` or `bigint`. A `number` is returned for safe numbers and decimal values that only lose some insignificant digits. A `bigint` is returned for large integer numbers. An `Error` is thrown for values that will overflow or underflow. Examples: | ||
```js | ||
// a safe number | ||
console.log(new LosslessNumber('23.4').valueOf()) | ||
// number 23.4 | ||
// a decimal losing insignificant digits | ||
console.log(new LosslessNumber('0.66666666666666666666667').valueOf()) | ||
// number 0.6666666666666666 | ||
// a large integer | ||
console.log(new LosslessNumber('9223372036854775827').valueOf()) | ||
// bigint 9223372036854775827 | ||
// a value that will overflow | ||
console.log(new LosslessNumber('2.3e+500').valueOf()) | ||
// Error: Cannot safely convert to number: the value '2.3e+500' would overflow and become Infinity | ||
// a value that will underflow | ||
console.log(new LosslessNumber('2.3e-500').valueOf()) | ||
// Error: Cannot safely convert to number: the value '2.3e-500' would underflow and become 0 | ||
``` | ||
Note that you can implement your own strategy for conversion by just getting the value as string via `.toString()`, and using util functions like `isInteger`, `isSafeNumber`, `getUnsafeNumberReason`, and `toSafeNumberOrThrow` to convert it to a numeric value. | ||
- `.toString() : string` | ||
Get the string representation of the lossless number. | ||
#### Properties | ||
- `{boolean} isLosslessNumber : true` | ||
- `{boolean} .isLosslessNumber : true` | ||
Lossless numbers contain a property `isLosslessNumber` which can be used to | ||
check whether some variable contains LosslessNumber. | ||
### Utility functions | ||
- `isInteger(value: string) : boolean` | ||
Test whether a string contains an integer value, like `'2300'` or `10`. | ||
- `isNumber(value: string) : boolean` | ||
Test whether a string contains a numeric value, like `'2.4'` or `'1.4e+3'`. | ||
- `isSafeNumber(value: string, config?: { approx: boolean }): boolean` | ||
Test whether a string contains a numeric value which can be safely represented by a JavaScript `number` without losing any information. Returns false when digits would be truncated of an integer or decimal, or when the number would overflow or underflow. When passing `{ approx: true }` as config, the function will be less strict and allow losing insignificant digits of a decimal value. Examples: | ||
```js | ||
isSafeNumber('1.55e3') // true | ||
isSafeNumber('2e500') // false | ||
isSafeNumber('2e-500') // false | ||
isSafeNumber('9223372036854775827') // false | ||
isSafeNumber('0.66666666666666666667') // false | ||
isSafeNumber('9223372036854775827', { approx: true }) // false | ||
isSafeNumber('0.66666666666666666667', { approx: true }) // true | ||
``` | ||
- `toSafeNumberOrThrow(value: string, config?: { approx: boolean }) : number` | ||
Convert a string into a number when it is safe to do so, otherwise throw an informative error. | ||
- `getUnsafeNumberReason(value): UnsafeNumberReason | undefined` | ||
When the provided `value` is an unsafe number, describe what the reason is: `overflow`, `underflow`, `truncate_integer`, `truncate_float`. Returns `undefined` when the value is safe. | ||
- `isLosslessNumber(value: unknown) : boolean` | ||
Test whether a value is a `LosslessNumber`. | ||
- `toLosslessNumber(value: number) : LosslessNumber` | ||
Convert a `number` into a `LosslessNumber`. The function will throw an exception when the `number` is exceeding the maximum safe limit of 15 digits (hence being truncated itself) or is `NaN` or `Infinity`. | ||
- `parseLosslessNumber(value: string) : LosslessNumber` | ||
The default `numberParser` used by `parse`. Creates a `LosslessNumber` from a string containing a numeric value. | ||
- `parseNumberAndBigInt(value: string) : number | bigint` | ||
A custom `numberParser` that can be used by `parse`. The parser will convert integer values into `bigint`, and converts al other values into a regular `number`. | ||
- `reviveDate(key, value)` | ||
Revive strings containing an ISO 8601 date string into a JavaScript `Date` object. This reviver is not turned on by default because there is a small risk of parsing a text field that _accidentally_ contains a date into a `Date`. Whether `reviveDate` is safe to use depends on the use case. Usage: | ||
```js | ||
import { parse, reviveDate } from 'lossless-json' | ||
const data = parse('["2022-08-25T09:39:19.288Z"]', reviveDate) | ||
// output: | ||
// [ | ||
// new Date('2022-08-25T09:39:19.288Z') | ||
// ] | ||
``` | ||
An alternative solution is to stringify a `Date` in a specific recognizable object like `{'$date':'2022-08-25T09:39:19.288Z'}`, and use a reviver and replacer to turn this object into a `Date` and vice versa. | ||
## Alternatives | ||
Similar libraries: | ||
- https://github.com/sidorares/json-bigint | ||
- https://github.com/nicolasparada/js-json-bigint | ||
- https://github.com/epoberezkin/json-source-map | ||
## Test | ||
@@ -284,14 +362,30 @@ | ||
Then generate a bundle (some tests validate the created bundle): | ||
To run the unit tests: | ||
``` | ||
npm run build | ||
npm test | ||
``` | ||
Then run the tests: | ||
To build the library and run the unit tests and integration tests: | ||
``` | ||
npm test | ||
npm run build-and-test | ||
``` | ||
## Lint | ||
Run linting: | ||
``` | ||
npm run lint | ||
``` | ||
Fix linting issues automatically: | ||
``` | ||
npm run format | ||
``` | ||
## Benchmark | ||
To run a benchmark to compare the performance with the native `JSON` parser: | ||
@@ -305,4 +399,2 @@ | ||
## Build | ||
@@ -322,18 +414,15 @@ | ||
This will generate an ES5 compatible bundle `./dist/lossless-json.js` which can be executed in browsers and node.js. | ||
This will generate an ES module output and an UMD bundle in the folder `./.lib` which can be executed in browsers and node.js and used in the browser. | ||
## Deploy | ||
- Update version number in `package.json` | ||
- Update version number in `package.json`, and run `npm install` to update the version number in `package-lock.json` too. | ||
- Describe changes is `HISTORY.md` | ||
- run `npm test` to see whether everything works correctly. | ||
- run `build-and-test` to see whether everything works correctly. | ||
- merge changes from `develop` into `master` | ||
- create git tag and push it: `git tag v1.0.2 && git push --tags` | ||
- publish via `npm publish` (this will first run `npm test && npm run build` before actually publishing the library). | ||
- publish via `npm publish` (this will first build, test, and lint before actually publishing the library). | ||
## License | ||
Released under the [MIT license](LICENSE.md). | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
160512
48
945
419
Yes
26
1