|
1 | | -# JSON22 |
2 | | -JSON superset with an ability to serialize/deserialize classes |
| 1 | +# JSON22 - JSON with types |
| 2 | +The JSON22 is a superset of [JSON](https://tools.ietf.org/html/rfc7159) with an ability to serialize/deserialize classes and extended support for number variables |
| 3 | + |
| 4 | +## TL;DR |
| 5 | +### To there ... |
| 6 | +```javascript |
| 7 | +const value = { |
| 8 | + name: "Femistoclus", |
| 9 | + amount: 3873133n, |
| 10 | + debt: NaN, |
| 11 | + date: new Date('2022-01-01'), |
| 12 | +}; |
| 13 | +const string = JSON22.stringify(value); |
| 14 | +console.log(string); |
| 15 | +// => |
| 16 | +// { |
| 17 | +// "name": "Femistoclus" |
| 18 | +// "amount": 3873133n, |
| 19 | +// "debt": NaN, |
| 20 | +// "date": Date(1641513600000), |
| 21 | +// } |
| 22 | +``` |
| 23 | + |
| 24 | +### ... and back |
| 25 | +```javascript |
| 26 | +const string = `{ |
| 27 | + "name": "Femistoclus" |
| 28 | + "amount": 3873133n, |
| 29 | + "debt": NaN, |
| 30 | + "date": Date(1641513600000), |
| 31 | +}`; |
| 32 | +const value = JSON22.parse(string); |
| 33 | +console.log(typeof value.date, value.date.constructor.name); // => object Date |
| 34 | +console.log(typeof value.amount); // => bigint |
| 35 | +console.log(typeof value.debt, isNaN(value.debt)); // => number true |
| 36 | +``` |
| 37 | +## Motivation |
| 38 | +JSON format is good enough for everyday usage. There is some libraries trying to introduce syntax to make JSON closer |
| 39 | +to modern JavaScript, some libraries trying to introduce functions serialization. All that is not important and do not |
| 40 | +required for everyday usage. However, there is one think annoing me always - date values. |
| 41 | + |
| 42 | +We are serializing dates a lot and each time we parse it back we get a string. As a result we have to deal with the Date |
| 43 | +constructor manually each time. Even we are no need date as object we will have to format it out to make more user friendly. |
| 44 | +Otherwords we should care about dates additionally. |
| 45 | + |
| 46 | +But I'm lazy developer, I'll do everything to get rid of any additional careness. |
| 47 | + |
| 48 | +## API |
| 49 | +```typescript |
| 50 | +class JSON22 { |
| 51 | + static parse(text: string, options?: Json22ParseOptions): any; |
| 52 | + static stringify(value: any, options?: Json22StringifyOptions): string; |
| 53 | +} |
| 54 | + |
| 55 | +interface Json22ParseOptions { |
| 56 | + context?: Record<string, { new (...args: any) }>; // default { 'Date': Date } |
| 57 | + // To be extended |
| 58 | +} |
| 59 | + |
| 60 | +interface Json22StringifyOptions { |
| 61 | + // To be extended |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +## JSON Extensions |
| 66 | + |
| 67 | +### Numbers |
| 68 | + |
| 69 | +With JSON22 you can use `NaN`, `Infinity`, `-Infinity` values. It mean also this values will be stringified well |
| 70 | +in case it nested at an array or an object. |
| 71 | +```javascript |
| 72 | +JSON.stringify([42, NaN, Infinity, -Infinity]); // => [42, null, null, null] |
| 73 | +JSON22.stringify([42, NaN, Infinity, -Infinity]); // => [42, NaN, Infinity, -Infinity] |
| 74 | +``` |
| 75 | +```javascript |
| 76 | +JSON.stringify({ nan: NaN }); // => { "nan": null } |
| 77 | +JSON22.stringify({ nan: NaN }); // => { "nan": NaN } |
| 78 | +``` |
| 79 | + |
| 80 | +### BigInt |
| 81 | +JSON22 introduce support for BigInt values |
| 82 | +```javascript |
| 83 | +JSON.stringify({ bigint: 123n }); // => Uncaught TypeError: Do not know how to serialize a BigInt |
| 84 | +JSON22.stringify({ bigint: 123n }); // => { "bigint": 123n } |
| 85 | +JSON22.parse('{ "bigint": 123n }'); // => { bigint: 123n } |
| 86 | +``` |
| 87 | + |
| 88 | +### Trailing commas |
| 89 | +It was not planned, but parser implementation work well with trailing commas. |
| 90 | +We are not going to complicate parser code to avoid it. It looks useful. |
| 91 | + |
| 92 | +```javascript |
| 93 | +JSON.parse('[1, 2, 3, ]'); // => Uncaught SyntaxError: Unexpected token ] in JSON at position 9 |
| 94 | +JSON22.parse('[1, 2, 3, ]'); // => [1, 2, 3] |
| 95 | +``` |
| 96 | +```javascript |
| 97 | +JSON.parse('{ "a": 1, "b": 2, "c": 3, }'); // => Uncaught SyntaxError: Unexpected token } in JSON at position 26 |
| 98 | +JSON22.parse('{ "a": 1, "b": 2, "c": 3, }'); // => { a: 1, b:2, c:3 } |
| 99 | +``` |
| 100 | +### Typed values |
| 101 | +This is the most significant addition. It's allow you to serialize and deserialize any typed value. |
| 102 | +Out of the box it works well with date values. |
| 103 | +```javascript |
| 104 | +const date = new Date('2022-01-07'); |
| 105 | +JSON.stringify(date); // => '"2022-01-07T00:00:00.000Z"' |
| 106 | +JSON22.stringify(date); // => Date(1641513600000) |
| 107 | +``` |
| 108 | +```javascript |
| 109 | +const date = JSON22.parse('Date(1641513600000)'); |
| 110 | +console.log(typeof date, date instanceof Date); // => object true |
| 111 | +``` |
| 112 | +This behavior is based on the `valueOf` method which is defined at the Object class. |
| 113 | +In case JSON22 find the `valueOf` method return a value which is not equal of the object itself then it will produce |
| 114 | +constructor literal. The `valueOf` of the Date class return numeric date representation. |
| 115 | +It you'll call Date constructor with that value date will be sort of 'restored'. |
| 116 | + |
| 117 | +#### Custom valueOf implementation |
| 118 | +To match this behavior you may implement you own `valueOf` method at you custom class. |
| 119 | + |
| 120 | +Let's define a model class for demonstration |
| 121 | +```javascript |
| 122 | +class TypedModel { |
| 123 | + a; |
| 124 | + b; |
| 125 | + constructor(data) { |
| 126 | + this.a = data.a; |
| 127 | + this.b = data.b; |
| 128 | + } |
| 129 | + valueOf() { |
| 130 | + return { a: this.a, b: this.b }; |
| 131 | + } |
| 132 | +} |
| 133 | +``` |
| 134 | +That sort of classes will be serialised as typed objects |
| 135 | +```javascript |
| 136 | +const value = new TypedModel({ a: 1, b: 2 }); |
| 137 | +JSON22.stringify(value); // => TypedModel({ "a": 1, "b": 1 }) |
| 138 | +``` |
| 139 | +The `valueOf` methods may return any serializable values, even typed objects |
| 140 | +```javascript |
| 141 | +const value = new TypedModel({ a: 1, b: new Date('2022-01-07') }); |
| 142 | +JSON22.stringify(value); // => TypedModel({ "a": 1, "b": Date(1641513600000) }) |
| 143 | +``` |
| 144 | +#### Parsing context |
| 145 | +Typically, serialization and deserialization are processes separated by different environments. |
| 146 | +Like serialization at a backend and deserialization at a frontend and vice versa. |
| 147 | +So `TypedModel` we defined above should be shared between environments. |
| 148 | +Also `JSON22` parser should have a link to this class. In theory, we can push all such classes to a global scope. |
| 149 | +It is easy, however, it is not the best solution. It will produce global scope pollution, may cause naming conflicts, |
| 150 | +and it is not safe to allow parser to call any constructor from a global scope. That is why you should always pass |
| 151 | +deserialization context to parser. |
| 152 | +```javascript |
| 153 | +const string = 'TypedModel({ "a": 1, "b": Date(1641513600000) })'; |
| 154 | +JSON22.parse(string); // => Error: Constructor TypedModel not defined in the context |
| 155 | + |
| 156 | +const context = { 'TypedModel': TypedModel }; |
| 157 | +const value = JSON22.parse(string, { context }); |
| 158 | +console.log(value instanceof TypedModel); // => true |
| 159 | +``` |
| 160 | + |
| 161 | +#### The `valueOf` method priority |
| 162 | +The `JSON22` support for `toJSON` method of an object as well as `JSON`. In some cases an object may have both `valueOf` |
| 163 | +and `toJSON` methods. Typical example is the Date class. The `JSON22` at first is a solution to serialize/deserialize |
| 164 | +date values, so `valueOf` have higher priority then `toJSON`. This is also true for any object implementing `valueOf` |
| 165 | +and `toJSON` both. |
0 commit comments