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.
- package/.github/dependabot.yml +7 -10
- package/.github/workflows/benchmark.yml +9 -4
- package/.github/workflows/ci.yml +2 -2
- package/README.md +59 -3
- package/bench.js +69 -7
- package/example.js +1 -1
- package/index.d.ts +4 -0
- package/index.js +99 -24
- package/package.json +6 -4
- package/test/array.test.js +46 -2
- package/test/if-then-else.test.js +2 -2
- package/test/nullable.test.js +15 -15
- package/test/recursion.test.js +65 -0
- package/test/typebox.test.js +36 -0
package/.github/dependabot.yml
CHANGED
|
@@ -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:
|
|
6
|
+
interval: "monthly"
|
|
11
7
|
open-pull-requests-limit: 10
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
|
|
9
|
+
- package-ecosystem: "npm"
|
|
10
|
+
directory: "/"
|
|
14
11
|
schedule:
|
|
15
|
-
interval:
|
|
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@
|
|
24
|
+
- uses: actions/checkout@v3
|
|
23
25
|
with:
|
|
24
|
-
|
|
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@
|
|
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@
|
|
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
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -19,10 +19,10 @@ jobs:
|
|
|
19
19
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
|
20
20
|
|
|
21
21
|
steps:
|
|
22
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@v3
|
|
23
23
|
|
|
24
24
|
- name: Use Node.js
|
|
25
|
-
uses: actions/setup-node@
|
|
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="#
|
|
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
|
|
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 (
|
|
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 <
|
|
124
|
-
multiArray
|
|
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
|
-
|
|
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
package/index.d.ts
CHANGED
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://
|
|
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
|
|
612
|
+
externalSchema
|
|
585
613
|
}
|
|
586
614
|
}
|
|
587
615
|
|
|
588
616
|
// Split file from walk
|
|
589
617
|
ref = ref.split('#')
|
|
590
618
|
|
|
591
|
-
//
|
|
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
|
|
603
|
-
root
|
|
604
|
-
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
|
|
632
|
-
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
|
|
654
|
-
root
|
|
655
|
-
externalSchema
|
|
691
|
+
schema,
|
|
692
|
+
root,
|
|
693
|
+
externalSchema
|
|
656
694
|
})
|
|
657
695
|
}
|
|
658
696
|
|
|
659
697
|
return {
|
|
660
698
|
schema: result,
|
|
661
|
-
root
|
|
662
|
-
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
|
|
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
|
|
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(${
|
|
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
|
|
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(${
|
|
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(${
|
|
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
|
|
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": "^
|
|
41
|
-
"tap": "^
|
|
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.
|
|
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
|
},
|
package/test/array.test.js
CHANGED
|
@@ -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
|
|
264
|
+
schema,
|
|
265
265
|
input: fooBarInput,
|
|
266
266
|
expected: foobarOutput
|
|
267
267
|
},
|
|
268
268
|
{
|
|
269
269
|
name: 'greeting',
|
|
270
|
-
schema
|
|
270
|
+
schema,
|
|
271
271
|
input: greetingInput,
|
|
272
272
|
expected: greetingOutput
|
|
273
273
|
},
|
package/test/nullable.test.js
CHANGED
|
@@ -9,11 +9,11 @@ const nullable = true
|
|
|
9
9
|
const complexObject = {
|
|
10
10
|
type: 'object',
|
|
11
11
|
properties: {
|
|
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
|
|
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
|
|
29
|
-
nullableNumber: { type: 'number', nullable
|
|
30
|
-
nullableInteger: { type: 'integer', nullable
|
|
31
|
-
nullableBoolean: { type: 'boolean', nullable
|
|
32
|
-
nullableNull: { type: 'null', 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
|
|
90
|
-
nullableNumber: [{ type: 'number', nullable
|
|
91
|
-
nullableInteger: [{ type: 'integer', nullable
|
|
92
|
-
nullableBoolean: [{ type: 'boolean', nullable
|
|
93
|
-
nullableNull: [{ type: 'null', nullable
|
|
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,
|
package/test/recursion.test.js
CHANGED
|
@@ -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
|
+
})
|