fast-json-stringify 3.0.2 → 3.2.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.
@@ -1,16 +1,13 @@
1
1
  version: 2
2
2
  updates:
3
- - package-ecosystem: github-actions
4
- directory: '/'
5
- ignore:
6
- - dependency-name: "actions/*"
7
- update-types:
8
- ["version-update:semver-minor", "version-update:semver-patch"]
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
9
5
  schedule:
10
- interval: daily
6
+ interval: "monthly"
11
7
  open-pull-requests-limit: 10
12
- - package-ecosystem: npm
13
- directory: '/'
8
+
9
+ - package-ecosystem: "npm"
10
+ directory: "/"
14
11
  schedule:
15
- interval: daily
12
+ interval: "weekly"
16
13
  open-pull-requests-limit: 10
@@ -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 }}
@@ -19,12 +21,13 @@ jobs:
19
21
  matrix:
20
22
  node-version: [12, 14, 16]
21
23
  steps:
22
- - uses: actions/checkout@v2
24
+ - uses: actions/checkout@v3
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
- - uses: actions/setup-node@v2
30
+ - uses: actions/setup-node@v3
28
31
  with:
29
32
  node-version: ${{ matrix.node-version }}
30
33
 
@@ -43,7 +46,7 @@ jobs:
43
46
  echo "::set-output name=BENCH_RESULT${{matrix.node-version}}::$content"
44
47
 
45
48
  # main benchmark
46
- - uses: actions/checkout@v2
49
+ - uses: actions/checkout@v3
47
50
  with:
48
51
  ref: 'master'
49
52
 
@@ -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
@@ -19,10 +19,10 @@ jobs:
19
19
  os: [macos-latest, ubuntu-latest, windows-latest]
20
20
 
21
21
  steps:
22
- - uses: actions/checkout@v2
22
+ - uses: actions/checkout@v3
23
23
 
24
24
  - name: Use Node.js
25
- uses: actions/setup-node@v2
25
+ uses: actions/setup-node@v3
26
26
  with:
27
27
  node-version: ${{ matrix.node-version }}
28
28
 
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.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { Options as AjvOptions } from "ajv"
2
2
  declare namespace build {
3
3
  interface BaseSchema {
4
+ /**
5
+ * Schema id
6
+ */
7
+ $id?: string
4
8
  /**
5
9
  * Schema title
6
10
  */
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) {
@@ -44,15 +52,18 @@ function mergeLocation (source, dest) {
44
52
 
45
53
  const arrayItemsReferenceSerializersMap = new Map()
46
54
  const objectReferenceSerializersMap = new Map()
55
+ const schemaReferenceMap = new Map()
56
+
47
57
  let ajvInstance = null
48
58
 
49
59
  function build (schema, options) {
50
60
  arrayItemsReferenceSerializersMap.clear()
51
61
  objectReferenceSerializersMap.clear()
62
+ schemaReferenceMap.clear()
52
63
 
53
64
  options = options || {}
54
65
 
55
- ajvInstance = new Ajv({ ...options.ajv, strictSchema: false })
66
+ ajvInstance = new Ajv({ ...options.ajv, strictSchema: false, uriResolver: fastUri })
56
67
  ajvFormats(ajvInstance)
57
68
 
58
69
  isValidSchema(schema)
@@ -72,6 +83,22 @@ function build (schema, options) {
72
83
  }
73
84
  }
74
85
 
86
+ if (options.largeArrayMechanism) {
87
+ if (validLargeArrayMechanisms.includes(options.largeArrayMechanism)) {
88
+ largeArrayMechanism = options.largeArrayMechanism
89
+ } else {
90
+ throw new Error(`Unsupported large array mechanism ${options.rounding}`)
91
+ }
92
+ }
93
+
94
+ if (options.largeArraySize) {
95
+ if (!Number.isNaN(Number.parseInt(options.largeArraySize, 10))) {
96
+ largeArraySize = options.largeArraySize
97
+ } else {
98
+ throw new Error(`Unsupported large array size. Expected integer-like, got ${options.largeArraySize}`)
99
+ }
100
+ }
101
+
75
102
  /* eslint no-new-func: "off" */
76
103
  let code = `
77
104
  'use strict'
@@ -150,6 +177,7 @@ function build (schema, options) {
150
177
 
151
178
  arrayItemsReferenceSerializersMap.clear()
152
179
  objectReferenceSerializersMap.clear()
180
+ schemaReferenceMap.clear()
153
181
 
154
182
  return (Function.apply(null, dependenciesName).apply(null, dependencies))
155
183
  }
@@ -189,7 +217,7 @@ const numberKeywords = [
189
217
 
190
218
  /**
191
219
  * Infer type based on keyword in order to generate optimized code
192
- * https://json-schema.org/latest/json-schema-validation.html#rfc.section.6
220
+ * https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6
193
221
  */
194
222
  function inferTypeByKeyword (schema) {
195
223
  // eslint-disable-next-line
@@ -581,15 +609,25 @@ function refFinder (ref, location) {
581
609
  return {
582
610
  schema: externalSchema[ref],
583
611
  root: externalSchema[ref],
584
- externalSchema: externalSchema
612
+ externalSchema
585
613
  }
586
614
  }
587
615
 
588
616
  // Split file from walk
589
617
  ref = ref.split('#')
590
618
 
591
- // If external file
592
- if (ref[0]) {
619
+ // Check schemaReferenceMap for $id entry
620
+ if (ref[0] && schemaReferenceMap.has(ref[0])) {
621
+ schema = schemaReferenceMap.get(ref[0])
622
+ root = schemaReferenceMap.get(ref[0])
623
+ if (schema.$ref) {
624
+ return refFinder(schema.$ref, {
625
+ schema,
626
+ root,
627
+ externalSchema
628
+ })
629
+ }
630
+ } else if (ref[0]) { // If external file
593
631
  schema = externalSchema[ref[0]]
594
632
  root = externalSchema[ref[0]]
595
633
 
@@ -599,9 +637,9 @@ function refFinder (ref, location) {
599
637
 
600
638
  if (schema.$ref) {
601
639
  return refFinder(schema.$ref, {
602
- schema: schema,
603
- root: root,
604
- externalSchema: externalSchema
640
+ schema,
641
+ root,
642
+ externalSchema
605
643
  })
606
644
  }
607
645
  }
@@ -628,8 +666,8 @@ function refFinder (ref, location) {
628
666
 
629
667
  return {
630
668
  schema: dereferenced,
631
- root: root,
632
- externalSchema: externalSchema
669
+ root,
670
+ externalSchema
633
671
  }
634
672
  } else {
635
673
  // eslint-disable-next-line
@@ -650,16 +688,16 @@ function refFinder (ref, location) {
650
688
 
651
689
  if (result.$ref) {
652
690
  return refFinder(result.$ref, {
653
- schema: schema,
654
- root: root,
655
- externalSchema: externalSchema
691
+ schema,
692
+ root,
693
+ externalSchema
656
694
  })
657
695
  }
658
696
 
659
697
  return {
660
698
  schema: result,
661
- root: root,
662
- externalSchema: externalSchema
699
+ root,
700
+ externalSchema
663
701
  }
664
702
 
665
703
  function findBadKey (obj, keys) {
@@ -790,7 +828,7 @@ function buildCode (location, code, laterCode, name) {
790
828
  laterCode = builtCode.laterCode
791
829
  }
792
830
 
793
- return { code: code, laterCode: laterCode }
831
+ return { code, laterCode }
794
832
  }
795
833
 
796
834
  function filterRequired (required, key) {
@@ -814,7 +852,7 @@ function buildCodeWithAllOfs (location, code, laterCode, name) {
814
852
  laterCode = builtCode.laterCode
815
853
  }
816
854
 
817
- return { code: code, laterCode: laterCode }
855
+ return { code, laterCode }
818
856
  }
819
857
 
820
858
  function buildInnerObject (location, name) {
@@ -849,7 +887,7 @@ function addIfThenElse (location, name) {
849
887
  ajvInstance.addSchema(i, schemaKey)
850
888
 
851
889
  code += `
852
- valid = ajv.validate(${JSON.stringify(schemaKey)}, obj)
890
+ valid = ajv.validate("${schemaKey}", obj)
853
891
  if (valid) {
854
892
  `
855
893
  if (merged.if && merged.then) {
@@ -885,7 +923,7 @@ function addIfThenElse (location, name) {
885
923
  code += `
886
924
  }
887
925
  `
888
- return { code: code, laterCode: laterCode }
926
+ return { code, laterCode }
889
927
  }
890
928
 
891
929
  function toJSON (variableName) {
@@ -897,7 +935,9 @@ function toJSON (variableName) {
897
935
 
898
936
  function buildObject (location, code, name) {
899
937
  const schema = location.schema
900
-
938
+ if (schema.$id !== undefined) {
939
+ schemaReferenceMap.set(schema.$id, schema)
940
+ }
901
941
  code += `
902
942
  function ${name} (input) {
903
943
  `
@@ -909,7 +949,7 @@ function buildObject (location, code, name) {
909
949
  `
910
950
  }
911
951
 
912
- if (objectReferenceSerializersMap.has(schema)) {
952
+ if (objectReferenceSerializersMap.has(schema) && objectReferenceSerializersMap.get(schema) !== name) {
913
953
  code += `
914
954
  return ${objectReferenceSerializersMap.get(schema)}(input)
915
955
  }
@@ -947,6 +987,9 @@ function buildObject (location, code, name) {
947
987
 
948
988
  function buildArray (location, code, name, key = null) {
949
989
  let schema = location.schema
990
+ if (schema.$id !== undefined) {
991
+ schemaReferenceMap.set(schema.$id, schema)
992
+ }
950
993
  code += `
951
994
  function ${name} (obj) {
952
995
  `
@@ -1028,6 +1071,11 @@ function buildArray (location, code, name, key = null) {
1028
1071
 
1029
1072
  code += `
1030
1073
  var l = obj.length
1074
+ if (l && l >= ${largeArraySize}) {`
1075
+
1076
+ const concatSnippet = `
1077
+ }
1078
+
1031
1079
  var jsonOutput= ''
1032
1080
  for (var i = 0; i < l; i++) {
1033
1081
  var json = ''
@@ -1039,7 +1087,25 @@ function buildArray (location, code, name, key = null) {
1039
1087
  }
1040
1088
  }
1041
1089
  return \`[\${jsonOutput}]\`
1090
+ }`
1091
+
1092
+ switch (largeArrayMechanism) {
1093
+ case 'default':
1094
+ code += `
1095
+ return \`[\${obj.map(${result.mapFnName}).join(',')}]\``
1096
+ break
1097
+
1098
+ case 'json-stringify':
1099
+ code += `
1100
+ return JSON.stringify(obj)`
1101
+ break
1102
+
1103
+ default:
1104
+ throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
1042
1105
  }
1106
+
1107
+ code += `
1108
+ ${concatSnippet}
1043
1109
  ${result.laterCode}
1044
1110
  `
1045
1111
 
@@ -1147,22 +1213,27 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1147
1213
 
1148
1214
  switch (type) {
1149
1215
  case 'null':
1216
+ funcName = '$asNull'
1150
1217
  code += `
1151
1218
  json += $asNull()
1152
1219
  `
1153
1220
  break
1154
1221
  case 'string': {
1222
+ funcName = '$asString'
1155
1223
  const stringSerializer = getStringSerializer(schema.format)
1156
1224
  code += nullable ? `json += obj${accessor} === null ? null : ${stringSerializer}(obj${accessor})` : `json += ${stringSerializer}(obj${accessor})`
1157
1225
  break
1158
1226
  }
1159
1227
  case 'integer':
1228
+ funcName = '$asInteger'
1160
1229
  code += nullable ? `json += obj${accessor} === null ? null : $asInteger(obj${accessor})` : `json += $asInteger(obj${accessor})`
1161
1230
  break
1162
1231
  case 'number':
1232
+ funcName = '$asNumber'
1163
1233
  code += nullable ? `json += obj${accessor} === null ? null : $asNumber(obj${accessor})` : `json += $asNumber(obj${accessor})`
1164
1234
  break
1165
1235
  case 'boolean':
1236
+ funcName = '$asBoolean'
1166
1237
  code += nullable ? `json += obj${accessor} === null ? null : $asBoolean(obj${accessor})` : `json += $asBoolean(obj${accessor})`
1167
1238
  break
1168
1239
  case 'object':
@@ -1180,6 +1251,7 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1180
1251
  `
1181
1252
  break
1182
1253
  case undefined:
1254
+ funcName = '$asNull'
1183
1255
  if ('anyOf' in schema) {
1184
1256
  // beware: dereferenceOfRefs has side effects and changes schema.anyOf
1185
1257
  const anyOfLocations = dereferenceOfRefs(location, 'anyOf')
@@ -1203,7 +1275,7 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1203
1275
  ajvInstance.addSchema(location.schema, schemaKey)
1204
1276
 
1205
1277
  code += `
1206
- ${index === 0 ? 'if' : 'else if'}(ajv.validate(${JSON.stringify(schemaKey)}, ${testValue}))
1278
+ ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
1207
1279
  ${nestedResult.code}
1208
1280
  `
1209
1281
  laterCode = nestedResult.laterCode
@@ -1224,7 +1296,7 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1224
1296
  ajvInstance.addSchema(location.schema, schemaKey)
1225
1297
 
1226
1298
  code += `
1227
- ${index === 0 ? 'if' : 'else if'}(ajv.validate(${JSON.stringify(schemaKey)}, ${testValue}))
1299
+ ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
1228
1300
  ${nestedResult.code}
1229
1301
  `
1230
1302
  laterCode = nestedResult.laterCode
@@ -1318,7 +1390,8 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1318
1390
 
1319
1391
  return {
1320
1392
  code,
1321
- laterCode
1393
+ laterCode,
1394
+ mapFnName: funcName
1322
1395
  }
1323
1396
  }
1324
1397
 
@@ -1334,6 +1407,8 @@ function isEmpty (schema) {
1334
1407
 
1335
1408
  module.exports = build
1336
1409
 
1410
+ module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
1411
+
1337
1412
  module.exports.restore = function ({ code, ajv }) {
1338
1413
  // eslint-disable-next-line
1339
1414
  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.2",
3
+ "version": "3.2.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -30,6 +30,7 @@
30
30
  },
31
31
  "homepage": "https://github.com/fastify/fast-json-stringify#readme",
32
32
  "devDependencies": {
33
+ "@sinclair/typebox": "^0.23.3",
33
34
  "benchmark": "^2.1.4",
34
35
  "compile-json-stringify": "^0.1.2",
35
36
  "is-my-json-valid": "^2.20.0",
@@ -37,15 +38,16 @@
37
38
  "pre-commit": "^1.2.2",
38
39
  "proxyquire": "^2.1.3",
39
40
  "semver": "^7.1.0",
40
- "standard": "^16.0.1",
41
- "tap": "^15.0.0",
41
+ "standard": "^17.0.0",
42
+ "tap": "^16.0.1",
42
43
  "typescript": "^4.0.2",
43
44
  "webpack": "^5.40.0"
44
45
  },
45
46
  "dependencies": {
46
- "ajv": "^8.6.2",
47
+ "ajv": "^8.10.0",
47
48
  "ajv-formats": "^2.1.1",
48
49
  "deepmerge": "^4.2.2",
50
+ "fast-uri": "^1.0.1",
49
51
  "rfdc": "^1.2.0",
50
52
  "string-similarity": "^4.0.1"
51
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
+ })
@@ -261,13 +261,13 @@ t.test('if-then-else', t => {
261
261
  const tests = [
262
262
  {
263
263
  name: 'foobar',
264
- schema: schema,
264
+ schema,
265
265
  input: fooBarInput,
266
266
  expected: foobarOutput
267
267
  },
268
268
  {
269
269
  name: 'greeting',
270
- schema: schema,
270
+ schema,
271
271
  input: greetingInput,
272
272
  expected: greetingOutput
273
273
  },
@@ -9,11 +9,11 @@ const nullable = true
9
9
  const complexObject = {
10
10
  type: 'object',
11
11
  properties: {
12
- nullableString: { type: 'string', nullable: nullable },
13
- nullableNumber: { type: 'number', nullable: nullable },
14
- nullableInteger: { type: 'integer', nullable: nullable },
15
- nullableBoolean: { type: 'boolean', nullable: nullable },
16
- nullableNull: { type: 'null', nullable: nullable },
12
+ nullableString: { type: 'string', nullable },
13
+ nullableNumber: { type: 'number', nullable },
14
+ nullableInteger: { type: 'integer', nullable },
15
+ nullableBoolean: { type: 'boolean', nullable },
16
+ nullableNull: { type: 'null', nullable },
17
17
  nullableArray: {
18
18
  type: 'array',
19
19
  nullable: true,
@@ -25,11 +25,11 @@ const complexObject = {
25
25
  nullable: false,
26
26
  additionalProperties: true,
27
27
  properties: {
28
- nullableString: { type: 'string', nullable: nullable },
29
- nullableNumber: { type: 'number', nullable: nullable },
30
- nullableInteger: { type: 'integer', nullable: nullable },
31
- nullableBoolean: { type: 'boolean', nullable: nullable },
32
- nullableNull: { type: 'null', nullable: nullable },
28
+ nullableString: { type: 'string', nullable },
29
+ nullableNumber: { type: 'number', nullable },
30
+ nullableInteger: { type: 'integer', nullable },
31
+ nullableBoolean: { type: 'boolean', nullable },
32
+ nullableNull: { type: 'null', nullable },
33
33
  nullableArray: {
34
34
  type: 'array',
35
35
  nullable: true,
@@ -86,11 +86,11 @@ const complexExpectedResult = {
86
86
  }
87
87
 
88
88
  const testSet = {
89
- nullableString: [{ type: 'string', nullable: nullable }, null, null],
90
- nullableNumber: [{ type: 'number', nullable: nullable }, null, null],
91
- nullableInteger: [{ type: 'integer', nullable: nullable }, null, null],
92
- nullableBoolean: [{ type: 'boolean', nullable: nullable }, null, null],
93
- nullableNull: [{ type: 'null', nullable: nullable }, null, null],
89
+ nullableString: [{ type: 'string', nullable }, null, null],
90
+ nullableNumber: [{ type: 'number', nullable }, null, null],
91
+ nullableInteger: [{ type: 'integer', nullable }, null, null],
92
+ nullableBoolean: [{ type: 'boolean', nullable }, null, null],
93
+ nullableNull: [{ type: 'null', nullable }, null, null],
94
94
  nullableArray: [{
95
95
  type: 'array',
96
96
  nullable: true,
@@ -178,3 +178,68 @@ test('can stringify recursive references in object types (issue #365)', t => {
178
178
  const value = stringify(data)
179
179
  t.equal(value, '{"category":{"parent":{"parent":{"parent":{"parent":{}}}}}}')
180
180
  })
181
+
182
+ test('can stringify recursive inline $id references (issue #410)', t => {
183
+ t.plan(1)
184
+ const schema = {
185
+ $id: 'Node',
186
+ type: 'object',
187
+ properties: {
188
+ id: {
189
+ type: 'string'
190
+ },
191
+ nodes: {
192
+ type: 'array',
193
+ items: {
194
+ $ref: 'Node'
195
+ }
196
+ }
197
+ },
198
+ required: [
199
+ 'id',
200
+ 'nodes'
201
+ ]
202
+ }
203
+
204
+ const stringify = build(schema)
205
+ const data = {
206
+ id: '0',
207
+ nodes: [
208
+ {
209
+ id: '1',
210
+ nodes: [{
211
+ id: '2',
212
+ nodes: [
213
+ { id: '3', nodes: [] },
214
+ { id: '4', nodes: [] },
215
+ { id: '5', nodes: [] }
216
+ ]
217
+ }]
218
+ },
219
+ {
220
+ id: '6',
221
+ nodes: [{
222
+ id: '7',
223
+ nodes: [
224
+ { id: '8', nodes: [] },
225
+ { id: '9', nodes: [] },
226
+ { id: '10', nodes: [] }
227
+ ]
228
+ }]
229
+ },
230
+ {
231
+ id: '11',
232
+ nodes: [{
233
+ id: '12',
234
+ nodes: [
235
+ { id: '13', nodes: [] },
236
+ { id: '14', nodes: [] },
237
+ { id: '15', nodes: [] }
238
+ ]
239
+ }]
240
+ }
241
+ ]
242
+ }
243
+ const value = stringify(data)
244
+ t.equal(value, '{"id":"0","nodes":[{"id":"1","nodes":[{"id":"2","nodes":[{"id":"3","nodes":[]},{"id":"4","nodes":[]},{"id":"5","nodes":[]}]}]},{"id":"6","nodes":[{"id":"7","nodes":[{"id":"8","nodes":[]},{"id":"9","nodes":[]},{"id":"10","nodes":[]}]}]},{"id":"11","nodes":[{"id":"12","nodes":[{"id":"13","nodes":[]},{"id":"14","nodes":[]},{"id":"15","nodes":[]}]}]}]}')
245
+ })
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const test = require('tap').test
4
+ const build = require('..')
5
+
6
+ test('nested object in pattern properties for typebox', (t) => {
7
+ const { Type } = require('@sinclair/typebox')
8
+
9
+ t.plan(1)
10
+
11
+ const nestedSchema = Type.Object({
12
+ nestedKey1: Type.String()
13
+ })
14
+
15
+ const RootSchema = Type.Object({
16
+ key1: Type.Record(Type.String(), nestedSchema),
17
+ key2: Type.Record(Type.String(), nestedSchema)
18
+ })
19
+
20
+ const schema = RootSchema
21
+ const stringify = build(schema)
22
+
23
+ const value = stringify({
24
+ key1: {
25
+ nestedKey: {
26
+ nestedKey1: 'value1'
27
+ }
28
+ },
29
+ key2: {
30
+ nestedKey: {
31
+ nestedKey1: 'value2'
32
+ }
33
+ }
34
+ })
35
+ t.equal(value, '{"key1":{"nestedKey":{"nestedKey1":"value1"}},"key2":{"nestedKey":{"nestedKey1":"value2"}}}')
36
+ })