fast-json-stringify 3.0.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,8 @@ jobs:
8
8
  benchmark:
9
9
  if: ${{ github.event.label.name == 'benchmark' }}
10
10
  runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
11
13
  outputs:
12
14
  PR-BENCH-12: ${{ steps.benchmark-pr.outputs.BENCH_RESULT12 }}
13
15
  PR-BENCH-14: ${{ steps.benchmark-pr.outputs.BENCH_RESULT14 }}
@@ -21,7 +23,8 @@ jobs:
21
23
  steps:
22
24
  - uses: actions/checkout@v2
23
25
  with:
24
- ref: ${{github.event.pull_request.head.ref}}
26
+ persist-credentials: false
27
+ ref: ${{github.event.pull_request.head.sha}}
25
28
  repository: ${{github.event.pull_request.head.repo.full_name}}
26
29
 
27
30
  - uses: actions/setup-node@v2
@@ -64,6 +67,8 @@ jobs:
64
67
  output-benchmark:
65
68
  needs: [benchmark]
66
69
  runs-on: ubuntu-latest
70
+ permissions:
71
+ pull-requests: write
67
72
  steps:
68
73
  - name: Comment PR
69
74
  uses: thollander/actions-comment-pull-request@v1
package/README.md CHANGED
@@ -56,11 +56,12 @@ compile-json-stringify date format x 1,086,187 ops/sec ±0.16% (99 runs sampled)
56
56
  - <a href="#missingFields">`Missing fields`</a>
57
57
  - <a href="#patternProperties">`Pattern Properties`</a>
58
58
  - <a href="#additionalProperties">`Additional Properties`</a>
59
- - <a href="#anyof">`AnyOf` and `OneOf`</a>
59
+ - <a href="#AnyOf-and-OneOf">`AnyOf` and `OneOf`</a>
60
60
  - <a href="#ref">`Reuse - $ref`</a>
61
61
  - <a href="#long">`Long integers`</a>
62
62
  - <a href="#integer">`Integers`</a>
63
63
  - <a href="#nullable">`Nullable`</a>
64
+ - <a href="#largearrays">`Large Arrays`</a>
64
65
  - <a href="#security">`Security Notice`</a>
65
66
  - <a href="#acknowledgements">`Acknowledgements`</a>
66
67
  - <a href="#license">`License`</a>
@@ -117,6 +118,8 @@ const stringify = fastJson(mySchema, {
117
118
  - `schema`: external schemas references by $ref property. [More details](#ref)
118
119
  - `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof)
119
120
  - `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
121
+ - `largeArrayMechanism`: set the mechanism that should be used to handle large
122
+ (by default `20000` or more items) arrays. [More details](#largearrays)
120
123
 
121
124
 
122
125
  <a name="api"></a>
@@ -268,7 +271,7 @@ const stringify = fastJson({
268
271
  const obj = {
269
272
  nickname: 'nick',
270
273
  matchfoo: 42,
271
- otherfoo: 'str'
274
+ otherfoo: 'str',
272
275
  matchnum: 3
273
276
  }
274
277
 
@@ -311,7 +314,7 @@ const stringify = fastJson({
311
314
  const obj = {
312
315
  nickname: 'nick',
313
316
  matchfoo: 42,
314
- otherfoo: 'str'
317
+ otherfoo: 'str',
315
318
  matchnum: 3,
316
319
  nomatchstr: 'valar morghulis',
317
320
  nomatchint: 313
@@ -582,6 +585,59 @@ Otherwise, instead of raising an error, null values will be coerced as follows:
582
585
  - `string` -> `""`
583
586
  - `boolean` -> `false`
584
587
 
588
+ <a name="largearrays"></a>
589
+ #### Large Arrays
590
+
591
+ Large arrays are, for the scope of this document, defined as arrays containing,
592
+ by default, `20000` elements or more. That value can be adjusted via the option
593
+ parameter `largeArraySize`.
594
+
595
+ At some point the overhead caused by the default mechanism used by
596
+ `fast-json-stringify` to handle arrays starts increasing exponentially, leading
597
+ to slow overall executions.
598
+
599
+ ##### Settings
600
+
601
+ In order to improve that the user can set the `largeArrayMechanism` and
602
+ `largeArraySize` options.
603
+
604
+ `largeArrayMechanism`'s default value is `default`. Valid values for it are:
605
+
606
+ - `default` - This option is a compromise between performance and feature set by
607
+ still providing the expected functionality out of this lib but giving up some
608
+ possible performance gain. With this option set, **large arrays** would be
609
+ stringified by joining their stringified elements using `Array.join` instead of
610
+ string concatenation for better performance
611
+ - `json-stringify` - This option will remove support for schema validation
612
+ within **large arrays** completely. By doing so the overhead previously
613
+ mentioned is nulled, greatly improving execution time. Mind there's no change
614
+ in behavior for arrays not considered _large_
615
+
616
+ `largeArraySize`'s default value is `20000`. Valid values for it are
617
+ integer-like values, such as:
618
+
619
+ - `20000`
620
+ - `2e4`
621
+ - `'20000'`
622
+ - `'2e4'` - _note this will be converted to `2`, not `20000`_
623
+ - `1.5` - _note this will be converted to `1`_
624
+
625
+ ##### Benchmarks
626
+
627
+ For reference, here goes some benchmarks for comparison over the three
628
+ mechanisms. Benchmarks conducted on an old machine.
629
+
630
+ - Machine: `ST1000LM024 HN-M 1TB HDD, Intel Core i7-3610QM @ 2.3GHz, 12GB RAM, 4C/8T`.
631
+ - Node.js `v16.13.1`
632
+
633
+ ```
634
+ JSON.stringify large array x 157 ops/sec ±0.73% (86 runs sampled)
635
+ fast-json-stringify large array default x 48.72 ops/sec ±4.92% (48 runs sampled)
636
+ fast-json-stringify large array json-stringify x 157 ops/sec ±0.76% (86 runs sampled)
637
+ compile-json-stringify large array x 175 ops/sec ±4.47% (79 runs sampled)
638
+ AJV Serialize large array x 58.76 ops/sec ±4.59% (60 runs sampled)
639
+ ```
640
+
585
641
  <a name="security"></a>
586
642
  ## Security notice
587
643
 
package/bench.js CHANGED
@@ -3,6 +3,10 @@
3
3
  const benchmark = require('benchmark')
4
4
  const suite = new benchmark.Suite()
5
5
 
6
+ const STR_LEN = 1e4
7
+ const LARGE_ARRAY_SIZE = 2e4
8
+ const MULTI_ARRAY_LENGHT = 1e3
9
+
6
10
  const schema = {
7
11
  title: 'Example Schema',
8
12
  type: 'object',
@@ -89,7 +93,8 @@ const obj = {
89
93
 
90
94
  const date = new Date()
91
95
 
92
- const multiArray = []
96
+ const multiArray = new Array(MULTI_ARRAY_LENGHT)
97
+ const largeArray = new Array(LARGE_ARRAY_SIZE)
93
98
 
94
99
  const CJS = require('compile-json-stringify')
95
100
  const CJSStringify = CJS(schemaCJS)
@@ -99,7 +104,10 @@ const CJSStringifyString = CJS({ type: 'string' })
99
104
 
100
105
  const FJS = require('.')
101
106
  const stringify = FJS(schema)
102
- const stringifyArray = FJS(arraySchema)
107
+ const stringifyArrayDefault = FJS(arraySchema)
108
+ const stringifyArrayJSONStringify = FJS(arraySchema, {
109
+ largeArrayMechanism: 'json-stringify'
110
+ })
103
111
  const stringifyDate = FJS(dateFormatSchema)
104
112
  const stringifyString = FJS({ type: 'string' })
105
113
  let str = ''
@@ -110,18 +118,48 @@ const ajvSerialize = ajv.compileSerializer(schemaAJVJTD)
110
118
  const ajvSerializeArray = ajv.compileSerializer(arraySchemaAJVJTD)
111
119
  const ajvSerializeString = ajv.compileSerializer({ type: 'string' })
112
120
 
121
+ const getRandomString = (length) => {
122
+ if (!Number.isInteger(length)) {
123
+ throw new Error('Expected integer length')
124
+ }
125
+
126
+ const validCharacters = 'abcdefghijklmnopqrstuvwxyz'
127
+ const nValidCharacters = 26
128
+
129
+ let result = ''
130
+ for (let i = 0; i < length; ++i) {
131
+ result += validCharacters[Math.floor(Math.random() * nValidCharacters)]
132
+ }
133
+
134
+ return result[0].toUpperCase() + result.slice(1)
135
+ }
136
+
113
137
  // eslint-disable-next-line
114
- for (var i = 0; i < 10000; i++) {
138
+ for (let i = 0; i < STR_LEN; i++) {
139
+ largeArray[i] = {
140
+ firstName: getRandomString(8),
141
+ lastName: getRandomString(6),
142
+ age: Math.ceil(Math.random() * 99)
143
+ }
144
+
115
145
  str += i
116
146
  if (i % 100 === 0) {
117
147
  str += '"'
118
148
  }
119
149
  }
120
150
 
151
+ for (let i = STR_LEN; i < LARGE_ARRAY_SIZE; ++i) {
152
+ largeArray[i] = {
153
+ firstName: getRandomString(10),
154
+ lastName: getRandomString(4),
155
+ age: Math.ceil(Math.random() * 99)
156
+ }
157
+ }
158
+
121
159
  Number(str)
122
160
 
123
- for (i = 0; i < 1000; i++) {
124
- multiArray.push(obj)
161
+ for (let i = 0; i < MULTI_ARRAY_LENGHT; i++) {
162
+ multiArray[i] = obj
125
163
  }
126
164
 
127
165
  suite.add('FJS creation', function () {
@@ -138,8 +176,12 @@ suite.add('JSON.stringify array', function () {
138
176
  JSON.stringify(multiArray)
139
177
  })
140
178
 
141
- suite.add('fast-json-stringify array', function () {
142
- stringifyArray(multiArray)
179
+ suite.add('fast-json-stringify array default', function () {
180
+ stringifyArrayDefault(multiArray)
181
+ })
182
+
183
+ suite.add('fast-json-stringify array json-stringify', function () {
184
+ stringifyArrayJSONStringify(multiArray)
143
185
  })
144
186
 
145
187
  suite.add('compile-json-stringify array', function () {
@@ -150,6 +192,26 @@ suite.add('AJV Serialize array', function () {
150
192
  ajvSerializeArray(multiArray)
151
193
  })
152
194
 
195
+ suite.add('JSON.stringify large array', function () {
196
+ JSON.stringify(largeArray)
197
+ })
198
+
199
+ suite.add('fast-json-stringify large array default', function () {
200
+ stringifyArrayDefault(largeArray)
201
+ })
202
+
203
+ suite.add('fast-json-stringify large array json-stringify', function () {
204
+ stringifyArrayJSONStringify(largeArray)
205
+ })
206
+
207
+ suite.add('compile-json-stringify large array', function () {
208
+ CJSStringifyArray(largeArray)
209
+ })
210
+
211
+ suite.add('AJV Serialize large array', function () {
212
+ ajvSerializeArray(largeArray)
213
+ })
214
+
153
215
  suite.add('JSON.stringify long string', function () {
154
216
  JSON.stringify(str)
155
217
  })
package/example.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const moment = require('moment')
2
- const fastJson = require('fast-json-stringify')
2
+ const fastJson = require('.')
3
3
  const stringify = fastJson({
4
4
  title: 'Example Schema',
5
5
  type: 'object',
package/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  /* eslint no-prototype-builtins: 0 */
4
4
 
5
5
  const Ajv = require('ajv')
6
+ const fastUri = require('fast-uri')
6
7
  const ajvFormats = require('ajv-formats')
7
8
  const merge = require('deepmerge')
8
9
  const clone = require('rfdc')({ proto: true })
@@ -10,7 +11,14 @@ const fjsCloned = Symbol('fast-json-stringify.cloned')
10
11
  const { randomUUID } = require('crypto')
11
12
 
12
13
  const validate = require('./schema-validator')
14
+
15
+ let largeArraySize = 2e4
13
16
  let stringSimilarity = null
17
+ let largeArrayMechanism = 'default'
18
+ const validLargeArrayMechanisms = [
19
+ 'default',
20
+ 'json-stringify'
21
+ ]
14
22
 
15
23
  const addComma = `
16
24
  if (addComma) {
@@ -52,7 +60,7 @@ function build (schema, options) {
52
60
 
53
61
  options = options || {}
54
62
 
55
- ajvInstance = new Ajv({ ...options.ajv, strictSchema: false })
63
+ ajvInstance = new Ajv({ ...options.ajv, strictSchema: false, uriResolver: fastUri })
56
64
  ajvFormats(ajvInstance)
57
65
 
58
66
  isValidSchema(schema)
@@ -72,6 +80,22 @@ function build (schema, options) {
72
80
  }
73
81
  }
74
82
 
83
+ if (options.largeArrayMechanism) {
84
+ if (validLargeArrayMechanisms.includes(options.largeArrayMechanism)) {
85
+ largeArrayMechanism = options.largeArrayMechanism
86
+ } else {
87
+ throw new Error(`Unsupported large array mechanism ${options.rounding}`)
88
+ }
89
+ }
90
+
91
+ if (options.largeArraySize) {
92
+ if (!Number.isNaN(Number.parseInt(options.largeArraySize, 10))) {
93
+ largeArraySize = options.largeArraySize
94
+ } else {
95
+ throw new Error(`Unsupported large array size. Expected integer-like, got ${options.largeArraySize}`)
96
+ }
97
+ }
98
+
75
99
  /* eslint no-new-func: "off" */
76
100
  let code = `
77
101
  'use strict'
@@ -189,7 +213,7 @@ const numberKeywords = [
189
213
 
190
214
  /**
191
215
  * Infer type based on keyword in order to generate optimized code
192
- * https://json-schema.org/latest/json-schema-validation.html#rfc.section.6
216
+ * https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6
193
217
  */
194
218
  function inferTypeByKeyword (schema) {
195
219
  // eslint-disable-next-line
@@ -1028,6 +1052,11 @@ function buildArray (location, code, name, key = null) {
1028
1052
 
1029
1053
  code += `
1030
1054
  var l = obj.length
1055
+ if (l && l >= ${largeArraySize}) {`
1056
+
1057
+ const concatSnippet = `
1058
+ }
1059
+
1031
1060
  var jsonOutput= ''
1032
1061
  for (var i = 0; i < l; i++) {
1033
1062
  var json = ''
@@ -1039,7 +1068,25 @@ function buildArray (location, code, name, key = null) {
1039
1068
  }
1040
1069
  }
1041
1070
  return \`[\${jsonOutput}]\`
1071
+ }`
1072
+
1073
+ switch (largeArrayMechanism) {
1074
+ case 'default':
1075
+ code += `
1076
+ return \`[\${obj.map(${result.mapFnName}).join(',')}]\``
1077
+ break
1078
+
1079
+ case 'json-stringify':
1080
+ code += `
1081
+ return JSON.stringify(obj)`
1082
+ break
1083
+
1084
+ default:
1085
+ throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
1042
1086
  }
1087
+
1088
+ code += `
1089
+ ${concatSnippet}
1043
1090
  ${result.laterCode}
1044
1091
  `
1045
1092
 
@@ -1147,22 +1194,27 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1147
1194
 
1148
1195
  switch (type) {
1149
1196
  case 'null':
1197
+ funcName = '$asNull'
1150
1198
  code += `
1151
1199
  json += $asNull()
1152
1200
  `
1153
1201
  break
1154
1202
  case 'string': {
1203
+ funcName = '$asString'
1155
1204
  const stringSerializer = getStringSerializer(schema.format)
1156
1205
  code += nullable ? `json += obj${accessor} === null ? null : ${stringSerializer}(obj${accessor})` : `json += ${stringSerializer}(obj${accessor})`
1157
1206
  break
1158
1207
  }
1159
1208
  case 'integer':
1209
+ funcName = '$asInteger'
1160
1210
  code += nullable ? `json += obj${accessor} === null ? null : $asInteger(obj${accessor})` : `json += $asInteger(obj${accessor})`
1161
1211
  break
1162
1212
  case 'number':
1213
+ funcName = '$asNumber'
1163
1214
  code += nullable ? `json += obj${accessor} === null ? null : $asNumber(obj${accessor})` : `json += $asNumber(obj${accessor})`
1164
1215
  break
1165
1216
  case 'boolean':
1217
+ funcName = '$asBoolean'
1166
1218
  code += nullable ? `json += obj${accessor} === null ? null : $asBoolean(obj${accessor})` : `json += $asBoolean(obj${accessor})`
1167
1219
  break
1168
1220
  case 'object':
@@ -1180,6 +1232,7 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1180
1232
  `
1181
1233
  break
1182
1234
  case undefined:
1235
+ funcName = '$asNull'
1183
1236
  if ('anyOf' in schema) {
1184
1237
  // beware: dereferenceOfRefs has side effects and changes schema.anyOf
1185
1238
  const anyOfLocations = dereferenceOfRefs(location, 'anyOf')
@@ -1318,7 +1371,8 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1318
1371
 
1319
1372
  return {
1320
1373
  code,
1321
- laterCode
1374
+ laterCode,
1375
+ mapFnName: funcName
1322
1376
  }
1323
1377
  }
1324
1378
 
@@ -1334,6 +1388,8 @@ function isEmpty (schema) {
1334
1388
 
1335
1389
  module.exports = build
1336
1390
 
1391
+ module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
1392
+
1337
1393
  module.exports.restore = function ({ code, ajv }) {
1338
1394
  // eslint-disable-next-line
1339
1395
  return (Function.apply(null, ['ajv', code])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -39,14 +39,15 @@
39
39
  "proxyquire": "^2.1.3",
40
40
  "semver": "^7.1.0",
41
41
  "standard": "^16.0.1",
42
- "tap": "^15.0.0",
42
+ "tap": "^16.0.1",
43
43
  "typescript": "^4.0.2",
44
44
  "webpack": "^5.40.0"
45
45
  },
46
46
  "dependencies": {
47
- "ajv": "^8.6.2",
47
+ "ajv": "^8.10.0",
48
48
  "ajv-formats": "^2.1.1",
49
49
  "deepmerge": "^4.2.2",
50
+ "fast-uri": "^1.0.1",
50
51
  "rfdc": "^1.2.0",
51
52
  "string-similarity": "^4.0.1"
52
53
  },
@@ -5,12 +5,12 @@ const test = require('tap').test
5
5
  const validator = require('is-my-json-valid')
6
6
  const build = require('..')
7
7
 
8
- function buildTest (schema, toStringify) {
8
+ function buildTest (schema, toStringify, options) {
9
9
  test(`render a ${schema.title} as JSON`, (t) => {
10
10
  t.plan(3)
11
11
 
12
12
  const validate = validator(schema)
13
- const stringify = build(schema)
13
+ const stringify = build(schema, options)
14
14
  const output = stringify(toStringify)
15
15
 
16
16
  t.same(JSON.parse(output), toStringify)
@@ -319,3 +319,47 @@ test('object array with anyOf and symbol', (t) => {
319
319
  ])
320
320
  t.equal(value, '[{"name":"name-0","option":"Foo"},{"name":"name-1","option":"Bar"}]')
321
321
  })
322
+
323
+ const largeArray = new Array(2e4).fill({ a: 'test', b: 1 })
324
+ buildTest({
325
+ title: 'large array with default mechanism',
326
+ type: 'object',
327
+ properties: {
328
+ ids: {
329
+ type: 'array',
330
+ items: {
331
+ type: 'object',
332
+ properties: {
333
+ a: { type: 'string' },
334
+ b: { type: 'number' }
335
+ }
336
+ }
337
+ }
338
+ }
339
+ }, {
340
+ ids: largeArray
341
+ }, {
342
+ largeArraySize: 2e4,
343
+ largeArrayMechanism: 'default'
344
+ })
345
+
346
+ buildTest({
347
+ title: 'large array with json-stringify mechanism',
348
+ type: 'object',
349
+ properties: {
350
+ ids: {
351
+ type: 'array',
352
+ items: {
353
+ type: 'object',
354
+ properties: {
355
+ a: { type: 'string' },
356
+ b: { type: 'number' }
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }, {
362
+ ids: largeArray
363
+ }, {
364
+ largeArrayMechanism: 'json-stringify'
365
+ })