fast-json-stringify 6.1.1 → 6.3.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/workflows/ci.yml +5 -0
- package/README.md +4 -6
- package/benchmark/bench-cmp-lib.js +23 -9
- package/benchmark/bench-thread.js +24 -12
- package/index.js +419 -153
- package/lib/validator.js +6 -4
- package/package.json +13 -12
- package/test/array.test.js +28 -0
- package/test/json-add-comma.test.js +57 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -14,6 +14,11 @@ on:
|
|
|
14
14
|
- 'docs/**'
|
|
15
15
|
- '*.md'
|
|
16
16
|
|
|
17
|
+
# This allows a subsequently queued workflow run to interrupt previous runs
|
|
18
|
+
concurrency:
|
|
19
|
+
group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
17
22
|
permissions:
|
|
18
23
|
contents: read
|
|
19
24
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
__fast-json-stringify__ is significantly faster than `JSON.stringify()` for small payloads.
|
|
9
9
|
Its performance advantage shrinks as your payload grows.
|
|
10
|
-
It pairs well with [__flatstr__](https://www.npmjs.com/package/flatstr), which triggers a V8 optimization that improves performance when eventually converting the string to a `Buffer`.
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
### How it works
|
|
@@ -80,7 +79,6 @@ compile-json-stringify date format x 803,214 ops/sec ±0.23% (97 runs sampled)
|
|
|
80
79
|
|
|
81
80
|
|
|
82
81
|
<a name="example"></a>
|
|
83
|
-
Try it out on RunKit: <a href="https://runkit.com/npm/fast-json-stringify">https://runkit.com/npm/fast-json-stringify</a>
|
|
84
82
|
## Example
|
|
85
83
|
|
|
86
84
|
```js
|
|
@@ -348,9 +346,9 @@ const stringify = fastJson({
|
|
|
348
346
|
properties: {
|
|
349
347
|
'undecidedType': {
|
|
350
348
|
'anyOf': [{
|
|
351
|
-
|
|
349
|
+
type: 'string'
|
|
352
350
|
}, {
|
|
353
|
-
|
|
351
|
+
type: 'boolean'
|
|
354
352
|
}]
|
|
355
353
|
}
|
|
356
354
|
}
|
|
@@ -646,8 +644,8 @@ const stringify = fastJson({
|
|
|
646
644
|
type: 'object',
|
|
647
645
|
properties: {
|
|
648
646
|
'code': {
|
|
649
|
-
|
|
650
|
-
|
|
647
|
+
type: 'string',
|
|
648
|
+
format: 'unsafe'
|
|
651
649
|
}
|
|
652
650
|
}
|
|
653
651
|
})
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const suite = new
|
|
3
|
+
const { Bench } = require('tinybench')
|
|
4
|
+
const suite = new Bench({
|
|
5
|
+
name: 'Library Comparison Benchmarks',
|
|
6
|
+
setup: (_task, mode) => {
|
|
7
|
+
// Run the garbage collector before warmup at each cycle
|
|
8
|
+
if (mode === 'warmup' && typeof globalThis.gc === 'function') {
|
|
9
|
+
globalThis.gc()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
})
|
|
5
13
|
|
|
6
14
|
const STR_LEN = 1e4
|
|
7
15
|
const LARGE_ARRAY_SIZE = 2e4
|
|
@@ -76,13 +84,13 @@ const arraySchemaAJVJTD = {
|
|
|
76
84
|
const dateFormatSchema = {
|
|
77
85
|
description: 'Date of birth',
|
|
78
86
|
type: 'string',
|
|
79
|
-
format: '
|
|
87
|
+
format: 'datetime'
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
const dateFormatSchemaCJS = {
|
|
83
91
|
description: 'Date of birth',
|
|
84
92
|
type: 'string',
|
|
85
|
-
format: '
|
|
93
|
+
format: 'datetime'
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
const obj = {
|
|
@@ -300,10 +308,16 @@ suite.add('compile-json-stringify date format', function () {
|
|
|
300
308
|
CJSStringifyDate(date)
|
|
301
309
|
})
|
|
302
310
|
|
|
303
|
-
suite.
|
|
311
|
+
suite.run().then(() => {
|
|
312
|
+
for (const task of suite.tasks) {
|
|
313
|
+
const hz = task.result.throughput.mean // ops/sec
|
|
314
|
+
const rme = task.result.latency.rme // relative margin of error (%)
|
|
315
|
+
const samples = task.result.latency.df + 1 // degrees of freedom + 1 = sample count
|
|
304
316
|
|
|
305
|
-
|
|
317
|
+
const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 })
|
|
318
|
+
const formattedRme = rme.toFixed(2)
|
|
306
319
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
320
|
+
const output = `${task.name} x ${formattedHz} ops/sec ±${formattedRme}% (${samples} runs sampled)`
|
|
321
|
+
console.log(output)
|
|
322
|
+
}
|
|
323
|
+
}).catch(err => console.error(`Error: ${err.message}`))
|
|
@@ -2,20 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
const { workerData: benchmark, parentPort } = require('worker_threads')
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
Benchmark.options.minSamples = 100
|
|
5
|
+
const { Bench } = require('tinybench')
|
|
7
6
|
|
|
8
|
-
const
|
|
7
|
+
const bench = new Bench({
|
|
8
|
+
name: benchmark.name,
|
|
9
|
+
setup: (_task, mode) => {
|
|
10
|
+
// Run the garbage collector before warmup at each cycle
|
|
11
|
+
if (mode === 'warmup' && typeof globalThis.gc === 'function') {
|
|
12
|
+
globalThis.gc()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
})
|
|
9
16
|
|
|
10
17
|
const FJS = require('..')
|
|
11
18
|
const stringify = FJS(benchmark.schema)
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
20
|
+
bench.add(benchmark.name, () => {
|
|
21
|
+
stringify(benchmark.input)
|
|
22
|
+
}).run().then(() => {
|
|
23
|
+
const task = bench.tasks[0]
|
|
24
|
+
const hz = task.result.throughput.mean // ops/sec
|
|
25
|
+
const rme = task.result.latency.rme // relative margin of error (%)
|
|
26
|
+
const samples = task.result.latency.df + 1 // degrees of freedom + 1 = sample count
|
|
27
|
+
|
|
28
|
+
const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 })
|
|
29
|
+
const formattedRme = rme.toFixed(2)
|
|
30
|
+
|
|
31
|
+
const output = `${task.name} x ${formattedHz} ops/sec ±${formattedRme}% (${samples} runs sampled)`
|
|
32
|
+
parentPort.postMessage(output)
|
|
33
|
+
}).catch(err => parentPort.postMessage(`Error: ${err.message}`))
|
package/index.js
CHANGED
|
@@ -30,17 +30,17 @@ const asInteger = serializer.asInteger.bind(serializer)
|
|
|
30
30
|
|
|
31
31
|
`
|
|
32
32
|
|
|
33
|
-
const validRoundingMethods = [
|
|
33
|
+
const validRoundingMethods = new Set([
|
|
34
34
|
'floor',
|
|
35
35
|
'ceil',
|
|
36
36
|
'round',
|
|
37
37
|
'trunc'
|
|
38
|
-
]
|
|
38
|
+
])
|
|
39
39
|
|
|
40
|
-
const validLargeArrayMechanisms = [
|
|
40
|
+
const validLargeArrayMechanisms = new Set([
|
|
41
41
|
'default',
|
|
42
42
|
'json-stringify'
|
|
43
|
-
]
|
|
43
|
+
])
|
|
44
44
|
|
|
45
45
|
let schemaIdCounter = 0
|
|
46
46
|
|
|
@@ -94,6 +94,14 @@ function getSchemaId (schema, rootSchemaId) {
|
|
|
94
94
|
return rootSchemaId
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function getSafeSchemaRef (context, location) {
|
|
98
|
+
let schemaRef = location.getSchemaRef() || ''
|
|
99
|
+
if (schemaRef.startsWith(context.rootSchemaId)) {
|
|
100
|
+
schemaRef = schemaRef.replace(context.rootSchemaId, '') || '#'
|
|
101
|
+
}
|
|
102
|
+
return schemaRef
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
function build (schema, options) {
|
|
98
106
|
isValidSchema(schema)
|
|
99
107
|
|
|
@@ -107,7 +115,11 @@ function build (schema, options) {
|
|
|
107
115
|
refResolver: new RefResolver(),
|
|
108
116
|
rootSchemaId: schema.$id || `__fjs_root_${schemaIdCounter++}`,
|
|
109
117
|
validatorSchemasIds: new Set(),
|
|
110
|
-
mergedSchemasIds: new Map()
|
|
118
|
+
mergedSchemasIds: new Map(),
|
|
119
|
+
recursiveSchemas: new Set(),
|
|
120
|
+
recursivePaths: new Set(),
|
|
121
|
+
buildingSet: new Set(),
|
|
122
|
+
uid: 0
|
|
111
123
|
}
|
|
112
124
|
|
|
113
125
|
const schemaId = getSchemaId(schema, context.rootSchemaId)
|
|
@@ -127,13 +139,13 @@ function build (schema, options) {
|
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
if (options.rounding) {
|
|
130
|
-
if (!validRoundingMethods.
|
|
142
|
+
if (!validRoundingMethods.has(options.rounding)) {
|
|
131
143
|
throw new Error(`Unsupported integer rounding method ${options.rounding}`)
|
|
132
144
|
}
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
if (options.largeArrayMechanism) {
|
|
136
|
-
if (validLargeArrayMechanisms.
|
|
148
|
+
if (validLargeArrayMechanisms.has(options.largeArrayMechanism)) {
|
|
137
149
|
largeArrayMechanism = options.largeArrayMechanism
|
|
138
150
|
} else {
|
|
139
151
|
throw new Error(`Unsupported large array mechanism ${options.largeArrayMechanism}`)
|
|
@@ -141,11 +153,14 @@ function build (schema, options) {
|
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
if (options.largeArraySize) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
156
|
+
const largeArraySizeType = typeof options.largeArraySize
|
|
157
|
+
let parsedNumber
|
|
158
|
+
|
|
159
|
+
if (largeArraySizeType === 'string' && Number.isFinite((parsedNumber = Number.parseInt(options.largeArraySize, 10)))) {
|
|
160
|
+
largeArraySize = parsedNumber
|
|
161
|
+
} else if (largeArraySizeType === 'number' && Number.isInteger(options.largeArraySize)) {
|
|
147
162
|
largeArraySize = options.largeArraySize
|
|
148
|
-
} else if (
|
|
163
|
+
} else if (largeArraySizeType === 'bigint') {
|
|
149
164
|
largeArraySize = Number(options.largeArraySize)
|
|
150
165
|
} else {
|
|
151
166
|
throw new Error(`Unsupported large array size. Expected integer-like, got ${typeof options.largeArraySize} with value ${options.largeArraySize}`)
|
|
@@ -153,6 +168,7 @@ function build (schema, options) {
|
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
const location = new Location(schema, context.rootSchemaId)
|
|
171
|
+
detectRecursiveSchemas(context, location)
|
|
156
172
|
const code = buildValue(context, location, 'input')
|
|
157
173
|
|
|
158
174
|
let contextFunctionCode = `
|
|
@@ -283,19 +299,19 @@ function inferTypeByKeyword (schema) {
|
|
|
283
299
|
return schema.type
|
|
284
300
|
}
|
|
285
301
|
|
|
286
|
-
function buildExtraObjectPropertiesSerializer (context, location, addComma) {
|
|
302
|
+
function buildExtraObjectPropertiesSerializer (context, location, addComma, objVar) {
|
|
287
303
|
const schema = location.schema
|
|
288
304
|
const propertiesKeys = Object.keys(schema.properties || {})
|
|
289
305
|
|
|
290
306
|
let code = `
|
|
291
|
-
const
|
|
292
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
307
|
+
for (const key of Object.keys(${objVar})) {
|
|
293
308
|
if (
|
|
294
|
-
propertiesKeys.
|
|
295
|
-
|
|
296
|
-
typeof
|
|
297
|
-
typeof
|
|
309
|
+
${propertiesKeys.length > 0 ? propertiesKeys.map(k => `key === ${JSON.stringify(k)}`).join(' || ') + ' ||' : ''}
|
|
310
|
+
${objVar}[key] === undefined ||
|
|
311
|
+
typeof ${objVar}[key] === 'function' ||
|
|
312
|
+
typeof ${objVar}[key] === 'symbol'
|
|
298
313
|
) continue
|
|
314
|
+
const value = ${objVar}[key]
|
|
299
315
|
`
|
|
300
316
|
|
|
301
317
|
const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
|
|
@@ -341,7 +357,7 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma) {
|
|
|
341
357
|
return code
|
|
342
358
|
}
|
|
343
359
|
|
|
344
|
-
function buildInnerObject (context, location) {
|
|
360
|
+
function buildInnerObject (context, location, objVar) {
|
|
345
361
|
const schema = location.schema
|
|
346
362
|
|
|
347
363
|
const propertiesLocation = location.getPropertyLocation('properties')
|
|
@@ -355,75 +371,128 @@ function buildInnerObject (context, location) {
|
|
|
355
371
|
return required1 === required2 ? 0 : required1 ? -1 : 1
|
|
356
372
|
}
|
|
357
373
|
)
|
|
358
|
-
const hasRequiredProperties = requiredProperties.includes(propertiesKeys[0])
|
|
359
374
|
|
|
360
|
-
let code = '
|
|
375
|
+
let code = ''
|
|
361
376
|
|
|
362
377
|
for (const key of requiredProperties) {
|
|
363
378
|
if (!propertiesKeys.includes(key)) {
|
|
364
379
|
const sanitizedKey = JSON.stringify(key)
|
|
365
|
-
code += `if (
|
|
380
|
+
code += `if (${objVar}[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')\n`
|
|
366
381
|
}
|
|
367
382
|
}
|
|
368
383
|
|
|
369
|
-
code += '
|
|
384
|
+
code += 'json += JSON_STR_BEGIN_OBJECT\n'
|
|
370
385
|
|
|
386
|
+
const localUid = context.uid++
|
|
371
387
|
let addComma = ''
|
|
372
|
-
if (!hasRequiredProperties) {
|
|
373
|
-
code += 'let addComma = false\n'
|
|
374
|
-
addComma = '!addComma && (addComma = true) || (json += JSON_STR_COMMA)'
|
|
375
|
-
}
|
|
376
388
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
propertyLocation = resolveRef(context, propertyLocation)
|
|
381
|
-
}
|
|
389
|
+
if (requiredProperties.length > 0) {
|
|
390
|
+
// If we have required properties, we know that at least one property will be serialized.
|
|
391
|
+
// We can avoid the runtime check for the comma.
|
|
382
392
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const isRequired = requiredProperties.includes(key)
|
|
393
|
+
// The first property is required, so we don't need a comma.
|
|
394
|
+
// For the subsequent properties, we can blindly add a comma.
|
|
386
395
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
396
|
+
for (let i = 0; i < propertiesKeys.length; i++) {
|
|
397
|
+
const key = propertiesKeys[i]
|
|
398
|
+
const propertyLocation = propertiesLocation.getPropertyLocation(key)
|
|
399
|
+
|
|
400
|
+
let resolvedLocation = propertyLocation
|
|
401
|
+
if (propertyLocation.schema.$ref) {
|
|
402
|
+
resolvedLocation = resolveRef(context, propertyLocation)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const sanitizedKey = JSON.stringify(key)
|
|
406
|
+
const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
|
|
407
|
+
const defaultValue = resolvedLocation.schema.default
|
|
408
|
+
const isRequired = requiredProperties.includes(key)
|
|
409
|
+
|
|
410
|
+
// i === 0 means it's the first property, and it IS required (due to sort). No comma.
|
|
411
|
+
// i > 0 means it follows a required property (or a sequence starting with one). Unconditional comma.
|
|
412
|
+
const currentAddComma = i === 0 ? '' : 'json += JSON_STR_COMMA'
|
|
413
|
+
|
|
414
|
+
code += `
|
|
415
|
+
const ${value} = ${objVar}[${sanitizedKey}]
|
|
416
|
+
if (${value} !== undefined) {
|
|
417
|
+
${currentAddComma}
|
|
391
418
|
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
392
|
-
${buildValue(context,
|
|
419
|
+
${buildValue(context, resolvedLocation, `${value}`)}
|
|
393
420
|
}`
|
|
394
421
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
${
|
|
422
|
+
if (defaultValue !== undefined) {
|
|
423
|
+
code += ` else {
|
|
424
|
+
${currentAddComma}
|
|
398
425
|
json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
|
|
399
426
|
}
|
|
400
427
|
`
|
|
401
|
-
|
|
402
|
-
|
|
428
|
+
} else if (isRequired) {
|
|
429
|
+
code += ` else {
|
|
403
430
|
throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')
|
|
404
431
|
}
|
|
405
432
|
`
|
|
406
|
-
|
|
407
|
-
|
|
433
|
+
} else {
|
|
434
|
+
code += '\n'
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
addComma = 'json += JSON_STR_COMMA'
|
|
439
|
+
} else {
|
|
440
|
+
const needsRuntimeComma = propertiesKeys.length > 1 || schema.patternProperties || (schema.additionalProperties !== undefined && schema.additionalProperties !== false)
|
|
441
|
+
|
|
442
|
+
if (needsRuntimeComma) {
|
|
443
|
+
code += `let addComma_${localUid} = false\n`
|
|
444
|
+
addComma = `!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)`
|
|
408
445
|
}
|
|
409
446
|
|
|
410
|
-
|
|
411
|
-
|
|
447
|
+
for (const key of propertiesKeys) {
|
|
448
|
+
let propertyLocation = propertiesLocation.getPropertyLocation(key)
|
|
449
|
+
if (propertyLocation.schema.$ref) {
|
|
450
|
+
propertyLocation = resolveRef(context, propertyLocation)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const sanitizedKey = JSON.stringify(key)
|
|
454
|
+
const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
|
|
455
|
+
const defaultValue = propertyLocation.schema.default
|
|
456
|
+
const isRequired = requiredProperties.includes(key) // Should be false here but good to keep
|
|
457
|
+
|
|
458
|
+
code += `
|
|
459
|
+
const ${value} = ${objVar}[${sanitizedKey}]
|
|
460
|
+
if (${value} !== undefined) {
|
|
461
|
+
${addComma}
|
|
462
|
+
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
463
|
+
${buildValue(context, propertyLocation, `${value}`)}
|
|
464
|
+
}`
|
|
465
|
+
|
|
466
|
+
if (defaultValue !== undefined) {
|
|
467
|
+
code += ` else {
|
|
468
|
+
${addComma}
|
|
469
|
+
json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
|
|
470
|
+
}
|
|
471
|
+
`
|
|
472
|
+
} else if (isRequired) {
|
|
473
|
+
// Should not happen if requiredProperties.length === 0 but safety
|
|
474
|
+
code += ` else {
|
|
475
|
+
throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')
|
|
476
|
+
}
|
|
477
|
+
`
|
|
478
|
+
} else {
|
|
479
|
+
code += '\n'
|
|
480
|
+
}
|
|
412
481
|
}
|
|
413
482
|
}
|
|
414
483
|
|
|
415
484
|
if (schema.patternProperties || schema.additionalProperties) {
|
|
416
|
-
code += buildExtraObjectPropertiesSerializer(context, location, addComma)
|
|
485
|
+
code += buildExtraObjectPropertiesSerializer(context, location, addComma, objVar)
|
|
417
486
|
}
|
|
418
487
|
|
|
419
488
|
code += `
|
|
420
|
-
|
|
489
|
+
json += JSON_STR_END_OBJECT
|
|
421
490
|
`
|
|
422
491
|
return code
|
|
423
492
|
}
|
|
424
493
|
|
|
425
494
|
function mergeLocations (context, mergedSchemaId, mergedLocations) {
|
|
426
|
-
for (let i = 0
|
|
495
|
+
for (let i = 0, mergedLocationsLength = mergedLocations.length; i < mergedLocationsLength; i++) {
|
|
427
496
|
const location = mergedLocations[i]
|
|
428
497
|
const schema = location.schema
|
|
429
498
|
if (schema.$ref) {
|
|
@@ -485,40 +554,57 @@ function toJSON (variableName) {
|
|
|
485
554
|
`
|
|
486
555
|
}
|
|
487
556
|
|
|
488
|
-
function buildObject (context, location) {
|
|
557
|
+
function buildObject (context, location, input) {
|
|
489
558
|
const schema = location.schema
|
|
490
559
|
|
|
491
560
|
if (context.functionsNamesBySchema.has(schema)) {
|
|
492
|
-
|
|
561
|
+
const funcName = context.functionsNamesBySchema.get(schema)
|
|
562
|
+
return `json += ${funcName}(${input})`
|
|
493
563
|
}
|
|
494
564
|
|
|
495
|
-
const
|
|
496
|
-
context.functionsNamesBySchema.set(schema, functionName)
|
|
565
|
+
const nullable = schema.nullable === true
|
|
497
566
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
567
|
+
const schemaId = location.schemaId || ''
|
|
568
|
+
const jsonPointer = location.jsonPointer || ''
|
|
569
|
+
const fullPath = `${schemaId}#${jsonPointer}`
|
|
502
570
|
|
|
503
|
-
|
|
504
|
-
|
|
571
|
+
if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
|
|
572
|
+
const functionName = generateFuncName(context)
|
|
573
|
+
context.functionsNamesBySchema.set(schema, functionName)
|
|
505
574
|
|
|
506
|
-
|
|
507
|
-
functionCode += `
|
|
508
|
-
// ${schemaRef}
|
|
509
|
-
function ${functionName} (input) {
|
|
510
|
-
const obj = ${toJSON('input')}
|
|
511
|
-
${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_OBJECT' : ''}
|
|
575
|
+
const schemaRef = getSafeSchemaRef(context, location)
|
|
512
576
|
|
|
513
|
-
|
|
577
|
+
const functionCode = `
|
|
578
|
+
// ${schemaRef}
|
|
579
|
+
function ${functionName} (input) {
|
|
580
|
+
const obj = ${toJSON('input')}
|
|
581
|
+
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
|
|
582
|
+
let json = ''
|
|
583
|
+
|
|
584
|
+
${buildInnerObject(context, location, 'obj')}
|
|
585
|
+
return json
|
|
586
|
+
}
|
|
587
|
+
`
|
|
588
|
+
|
|
589
|
+
context.functions.push(functionCode)
|
|
590
|
+
return `json += ${functionName}(${input})`
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
context.buildingSet.add(schema)
|
|
594
|
+
const objVar = `obj_${context.uid++}`
|
|
595
|
+
const code = `
|
|
596
|
+
const ${objVar} = ${toJSON(input)}
|
|
597
|
+
if (${objVar} === null) {
|
|
598
|
+
json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
|
|
599
|
+
} else {
|
|
600
|
+
${buildInnerObject(context, location, objVar)}
|
|
514
601
|
}
|
|
515
602
|
`
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return functionName
|
|
603
|
+
context.buildingSet.delete(schema)
|
|
604
|
+
return code
|
|
519
605
|
}
|
|
520
606
|
|
|
521
|
-
function buildArray (context, location) {
|
|
607
|
+
function buildArray (context, location, input) {
|
|
522
608
|
const schema = location.schema
|
|
523
609
|
|
|
524
610
|
let itemsLocation = location.getPropertyLocation('items')
|
|
@@ -531,61 +617,155 @@ function buildArray (context, location) {
|
|
|
531
617
|
const itemsSchema = itemsLocation.schema
|
|
532
618
|
|
|
533
619
|
if (context.functionsNamesBySchema.has(schema)) {
|
|
534
|
-
|
|
620
|
+
const funcName = context.functionsNamesBySchema.get(schema)
|
|
621
|
+
return `json += ${funcName}(${input})`
|
|
535
622
|
}
|
|
536
623
|
|
|
537
|
-
const
|
|
538
|
-
context.functionsNamesBySchema.set(schema, functionName)
|
|
624
|
+
const nullable = schema.nullable === true
|
|
539
625
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
626
|
+
const schemaId = location.schemaId || ''
|
|
627
|
+
const jsonPointer = location.jsonPointer || ''
|
|
628
|
+
const fullPath = `${schemaId}#${jsonPointer}`
|
|
544
629
|
|
|
545
|
-
|
|
630
|
+
if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
|
|
631
|
+
const functionName = generateFuncName(context)
|
|
632
|
+
context.functionsNamesBySchema.set(schema, functionName)
|
|
633
|
+
|
|
634
|
+
const schemaRef = getSafeSchemaRef(context, location)
|
|
635
|
+
|
|
636
|
+
let functionCode = `
|
|
546
637
|
function ${functionName} (obj) {
|
|
547
638
|
// ${schemaRef}
|
|
639
|
+
let json = ''
|
|
548
640
|
`
|
|
549
641
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_ARRAY' : ''}
|
|
642
|
+
functionCode += `
|
|
643
|
+
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
|
|
553
644
|
if (!Array.isArray(obj)) {
|
|
554
645
|
throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
|
|
555
646
|
}
|
|
556
647
|
const arrayLength = obj.length
|
|
557
648
|
`
|
|
558
649
|
|
|
559
|
-
|
|
560
|
-
|
|
650
|
+
if (!schema.additionalItems && Array.isArray(itemsSchema)) {
|
|
651
|
+
functionCode += `
|
|
561
652
|
if (arrayLength > ${itemsSchema.length}) {
|
|
562
653
|
throw new Error(\`Item at ${itemsSchema.length} does not match schema definition.\`)
|
|
563
654
|
}
|
|
564
655
|
`
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (largeArrayMechanism === 'json-stringify') {
|
|
659
|
+
functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
functionCode += `
|
|
663
|
+
json += JSON_STR_BEGIN_ARRAY
|
|
664
|
+
`
|
|
665
|
+
|
|
666
|
+
if (Array.isArray(itemsSchema)) {
|
|
667
|
+
for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) {
|
|
668
|
+
let item = itemsSchema[i]
|
|
669
|
+
let itemLocation = itemsLocation.getPropertyLocation(i)
|
|
670
|
+
if (itemLocation.schema.$ref) {
|
|
671
|
+
itemLocation = resolveRef(context, itemLocation)
|
|
672
|
+
item = itemLocation.schema
|
|
673
|
+
}
|
|
674
|
+
const value = `value_${i}`
|
|
675
|
+
functionCode += `const ${value} = obj[${i}]`
|
|
676
|
+
const tmpRes = buildValue(context, itemLocation, value)
|
|
677
|
+
functionCode += `
|
|
678
|
+
if (${i} < arrayLength) {
|
|
679
|
+
if (${buildArrayTypeCondition(item.type, value)}) {
|
|
680
|
+
if (${i}) {
|
|
681
|
+
json += JSON_STR_COMMA
|
|
682
|
+
}
|
|
683
|
+
${tmpRes}
|
|
684
|
+
} else {
|
|
685
|
+
throw new Error(\`Item at ${i} does not match schema definition.\`)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
`
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (schema.additionalItems) {
|
|
692
|
+
functionCode += `
|
|
693
|
+
for (let i = ${itemsSchema.length}; i < arrayLength; i++) {
|
|
694
|
+
if (i) {
|
|
695
|
+
json += JSON_STR_COMMA
|
|
696
|
+
}
|
|
697
|
+
json += JSON.stringify(obj[i])
|
|
698
|
+
}`
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
const code = buildValue(context, itemsLocation, 'value')
|
|
702
|
+
functionCode += `
|
|
703
|
+
if (arrayLength > 0) {
|
|
704
|
+
const value = obj[0]
|
|
705
|
+
${code}
|
|
706
|
+
for (let i = 1; i < arrayLength; i++) {
|
|
707
|
+
json += JSON_STR_COMMA
|
|
708
|
+
const value = obj[i]
|
|
709
|
+
${code}
|
|
710
|
+
}
|
|
711
|
+
}`
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
functionCode += `
|
|
715
|
+
return json + JSON_STR_END_ARRAY
|
|
716
|
+
}`
|
|
717
|
+
|
|
718
|
+
context.functions.push(functionCode)
|
|
719
|
+
return `json += ${functionName}(${input})`
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
context.buildingSet.add(schema)
|
|
723
|
+
const safeSchemaRef = getSafeSchemaRef(context, location)
|
|
724
|
+
const objVar = `obj_${context.uid++}`
|
|
725
|
+
let inlinedCode = `
|
|
726
|
+
const ${objVar} = ${input}
|
|
727
|
+
if (${objVar} === null) {
|
|
728
|
+
json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
|
|
729
|
+
} else if (!Array.isArray(${objVar})) {
|
|
730
|
+
throw new TypeError(\`The value of '${safeSchemaRef}' does not match schema definition.\`)
|
|
731
|
+
} else {
|
|
732
|
+
const arrayLength_${objVar} = ${objVar}.length
|
|
733
|
+
`
|
|
734
|
+
|
|
735
|
+
if (!schema.additionalItems && Array.isArray(itemsSchema)) {
|
|
736
|
+
inlinedCode += `
|
|
737
|
+
if (arrayLength_${objVar} > ${itemsSchema.length}) {
|
|
738
|
+
throw new Error(\`Item at ${itemsSchema.length} does not match schema definition.\`)
|
|
739
|
+
}
|
|
740
|
+
`
|
|
565
741
|
}
|
|
566
742
|
|
|
567
743
|
if (largeArrayMechanism === 'json-stringify') {
|
|
568
|
-
|
|
744
|
+
inlinedCode += `if (arrayLength_${objVar} >= ${largeArraySize}) json += JSON.stringify(${objVar})\n else {`
|
|
569
745
|
}
|
|
570
746
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
let value
|
|
574
|
-
let json = ''
|
|
747
|
+
inlinedCode += `
|
|
748
|
+
json += JSON_STR_BEGIN_ARRAY
|
|
575
749
|
`
|
|
576
750
|
|
|
577
751
|
if (Array.isArray(itemsSchema)) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
752
|
+
const localUid = context.uid++
|
|
753
|
+
inlinedCode += `let addComma_${localUid} = false\n`
|
|
754
|
+
for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) {
|
|
755
|
+
let item = itemsSchema[i]
|
|
756
|
+
let itemLocation = itemsLocation.getPropertyLocation(i)
|
|
757
|
+
if (itemLocation.schema.$ref) {
|
|
758
|
+
itemLocation = resolveRef(context, itemLocation)
|
|
759
|
+
item = itemLocation.schema
|
|
760
|
+
}
|
|
761
|
+
const value = `value_${i}_${context.uid++}`
|
|
762
|
+
inlinedCode += `const ${value} = ${objVar}[${i}]`
|
|
763
|
+
const tmpRes = buildValue(context, itemLocation, value)
|
|
764
|
+
inlinedCode += `
|
|
765
|
+
if (${i} < arrayLength_${objVar}) {
|
|
766
|
+
if (${buildArrayTypeCondition(item.type, value)}) {
|
|
767
|
+
!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)
|
|
585
768
|
${tmpRes}
|
|
586
|
-
if (${i} < arrayEnd) {
|
|
587
|
-
json += JSON_STR_COMMA
|
|
588
|
-
}
|
|
589
769
|
} else {
|
|
590
770
|
throw new Error(\`Item at ${i} does not match schema definition.\`)
|
|
591
771
|
}
|
|
@@ -594,66 +774,70 @@ function buildArray (context, location) {
|
|
|
594
774
|
}
|
|
595
775
|
|
|
596
776
|
if (schema.additionalItems) {
|
|
597
|
-
|
|
598
|
-
for (let i = ${itemsSchema.length}; i <
|
|
599
|
-
|
|
600
|
-
json += JSON.stringify(
|
|
601
|
-
if (i < arrayEnd) {
|
|
602
|
-
json += JSON_STR_COMMA
|
|
603
|
-
}
|
|
777
|
+
inlinedCode += `
|
|
778
|
+
for (let i = ${itemsSchema.length}; i < arrayLength_${objVar}; i++) {
|
|
779
|
+
!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)
|
|
780
|
+
json += JSON.stringify(${objVar}[i])
|
|
604
781
|
}`
|
|
605
782
|
}
|
|
606
783
|
} else {
|
|
607
784
|
const code = buildValue(context, itemsLocation, 'value')
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
value =
|
|
785
|
+
inlinedCode += `
|
|
786
|
+
if (arrayLength_${objVar} > 0) {
|
|
787
|
+
const value = ${objVar}[0]
|
|
611
788
|
${code}
|
|
612
|
-
|
|
789
|
+
for (let i = 1; i < arrayLength_${objVar}; i++) {
|
|
613
790
|
json += JSON_STR_COMMA
|
|
791
|
+
const value = ${objVar}[i]
|
|
792
|
+
${code}
|
|
614
793
|
}
|
|
615
794
|
}`
|
|
616
795
|
}
|
|
617
796
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
797
|
+
inlinedCode += `
|
|
798
|
+
json += JSON_STR_END_ARRAY
|
|
799
|
+
`
|
|
621
800
|
|
|
622
|
-
|
|
623
|
-
|
|
801
|
+
if (largeArrayMechanism === 'json-stringify') {
|
|
802
|
+
inlinedCode += '}'
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
inlinedCode += '}'
|
|
806
|
+
context.buildingSet.delete(schema)
|
|
807
|
+
return inlinedCode
|
|
624
808
|
}
|
|
625
809
|
|
|
626
810
|
function buildArrayTypeCondition (type, accessor) {
|
|
627
811
|
let condition
|
|
628
812
|
switch (type) {
|
|
629
813
|
case 'null':
|
|
630
|
-
condition =
|
|
814
|
+
condition = `${accessor} === null`
|
|
631
815
|
break
|
|
632
816
|
case 'string':
|
|
633
|
-
condition = `typeof
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
817
|
+
condition = `typeof ${accessor} === 'string' ||
|
|
818
|
+
${accessor} === null ||
|
|
819
|
+
${accessor} instanceof Date ||
|
|
820
|
+
${accessor} instanceof RegExp ||
|
|
637
821
|
(
|
|
638
|
-
typeof
|
|
639
|
-
typeof
|
|
640
|
-
|
|
822
|
+
typeof ${accessor} === "object" &&
|
|
823
|
+
typeof ${accessor}.toString === "function" &&
|
|
824
|
+
${accessor}.toString !== Object.prototype.toString
|
|
641
825
|
)`
|
|
642
826
|
break
|
|
643
827
|
case 'integer':
|
|
644
|
-
condition =
|
|
828
|
+
condition = `Number.isInteger(${accessor})`
|
|
645
829
|
break
|
|
646
830
|
case 'number':
|
|
647
|
-
condition =
|
|
831
|
+
condition = `Number.isFinite(${accessor})`
|
|
648
832
|
break
|
|
649
833
|
case 'boolean':
|
|
650
|
-
condition =
|
|
834
|
+
condition = `typeof ${accessor} === 'boolean'`
|
|
651
835
|
break
|
|
652
836
|
case 'object':
|
|
653
|
-
condition =
|
|
837
|
+
condition = `${accessor} && typeof ${accessor} === 'object' && ${accessor}.constructor === Object`
|
|
654
838
|
break
|
|
655
839
|
case 'array':
|
|
656
|
-
condition =
|
|
840
|
+
condition = `Array.isArray(${accessor})`
|
|
657
841
|
break
|
|
658
842
|
default:
|
|
659
843
|
if (Array.isArray(type)) {
|
|
@@ -684,8 +868,9 @@ function buildMultiTypeSerializer (context, location, input) {
|
|
|
684
868
|
switch (type) {
|
|
685
869
|
case 'null':
|
|
686
870
|
code += `
|
|
687
|
-
${statement} (${input} === null)
|
|
871
|
+
${statement} (${input} === null) {
|
|
688
872
|
${nestedResult}
|
|
873
|
+
}
|
|
689
874
|
`
|
|
690
875
|
break
|
|
691
876
|
case 'string': {
|
|
@@ -700,40 +885,40 @@ function buildMultiTypeSerializer (context, location, input) {
|
|
|
700
885
|
typeof ${input}.toString === "function" &&
|
|
701
886
|
${input}.toString !== Object.prototype.toString
|
|
702
887
|
)
|
|
703
|
-
)
|
|
888
|
+
) {
|
|
704
889
|
${nestedResult}
|
|
890
|
+
}
|
|
705
891
|
`
|
|
706
892
|
break
|
|
707
893
|
}
|
|
708
894
|
case 'array': {
|
|
709
895
|
code += `
|
|
710
|
-
${statement}(Array.isArray(${input}))
|
|
896
|
+
${statement}(Array.isArray(${input})) {
|
|
711
897
|
${nestedResult}
|
|
898
|
+
}
|
|
712
899
|
`
|
|
713
900
|
break
|
|
714
901
|
}
|
|
715
902
|
case 'integer': {
|
|
716
903
|
code += `
|
|
717
|
-
${statement}(Number.isInteger(${input}) || ${input} === null)
|
|
904
|
+
${statement}(Number.isInteger(${input}) || ${input} === null) {
|
|
718
905
|
${nestedResult}
|
|
906
|
+
}
|
|
719
907
|
`
|
|
720
908
|
break
|
|
721
909
|
}
|
|
722
910
|
default: {
|
|
723
911
|
code += `
|
|
724
|
-
${statement}(typeof ${input} === "${type}" || ${input} === null)
|
|
912
|
+
${statement}(typeof ${input} === "${type}" || ${input} === null) {
|
|
725
913
|
${nestedResult}
|
|
914
|
+
}
|
|
726
915
|
`
|
|
727
916
|
break
|
|
728
917
|
}
|
|
729
918
|
}
|
|
730
919
|
})
|
|
731
|
-
let schemaRef = location.getSchemaRef()
|
|
732
|
-
if (schemaRef.startsWith(context.rootSchemaId)) {
|
|
733
|
-
schemaRef = schemaRef.replace(context.rootSchemaId, '')
|
|
734
|
-
}
|
|
735
920
|
code += `
|
|
736
|
-
else throw new TypeError(\`The value of '${
|
|
921
|
+
else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
|
|
737
922
|
`
|
|
738
923
|
|
|
739
924
|
return code
|
|
@@ -779,12 +964,10 @@ function buildSingleTypeSerializer (context, location, input) {
|
|
|
779
964
|
case 'boolean':
|
|
780
965
|
return `json += asBoolean(${input})`
|
|
781
966
|
case 'object': {
|
|
782
|
-
|
|
783
|
-
return `json += ${funcName}(${input})`
|
|
967
|
+
return buildObject(context, location, input)
|
|
784
968
|
}
|
|
785
969
|
case 'array': {
|
|
786
|
-
|
|
787
|
-
return `json += ${funcName}(${input})`
|
|
970
|
+
return buildArray(context, location, input)
|
|
788
971
|
}
|
|
789
972
|
case undefined:
|
|
790
973
|
return `json += JSON.stringify(${input})`
|
|
@@ -793,6 +976,92 @@ function buildSingleTypeSerializer (context, location, input) {
|
|
|
793
976
|
}
|
|
794
977
|
}
|
|
795
978
|
|
|
979
|
+
function detectRecursiveSchemas (context, location) {
|
|
980
|
+
const pathStack = new Set()
|
|
981
|
+
function traverse (location) {
|
|
982
|
+
const schema = location.schema
|
|
983
|
+
if (typeof schema !== 'object' || schema === null) return
|
|
984
|
+
|
|
985
|
+
const schemaId = location.schemaId || ''
|
|
986
|
+
const jsonPointer = location.jsonPointer || ''
|
|
987
|
+
const fullPath = `${schemaId}#${jsonPointer}`
|
|
988
|
+
|
|
989
|
+
if (pathStack.has(fullPath)) {
|
|
990
|
+
// Mark all nodes in the current path that are part of the cycle
|
|
991
|
+
let inCycle = false
|
|
992
|
+
for (const p of pathStack) {
|
|
993
|
+
if (p === fullPath) inCycle = true
|
|
994
|
+
if (inCycle) context.recursivePaths.add(p)
|
|
995
|
+
}
|
|
996
|
+
context.recursivePaths.add(fullPath)
|
|
997
|
+
return
|
|
998
|
+
}
|
|
999
|
+
pathStack.add(fullPath)
|
|
1000
|
+
|
|
1001
|
+
if (schema.$ref) {
|
|
1002
|
+
try {
|
|
1003
|
+
const res = resolveRef(context, location)
|
|
1004
|
+
traverse(res)
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
// Validation will handle missing refs later
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (schema.properties) {
|
|
1011
|
+
const propertiesLocation = location.getPropertyLocation('properties')
|
|
1012
|
+
for (const key in schema.properties) {
|
|
1013
|
+
traverse(propertiesLocation.getPropertyLocation(key))
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
1017
|
+
traverse(location.getPropertyLocation('additionalProperties'))
|
|
1018
|
+
}
|
|
1019
|
+
if (schema.patternProperties) {
|
|
1020
|
+
const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
|
|
1021
|
+
for (const key in schema.patternProperties) {
|
|
1022
|
+
traverse(patternPropertiesLocation.getPropertyLocation(key))
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (schema.items) {
|
|
1026
|
+
const itemsLocation = location.getPropertyLocation('items')
|
|
1027
|
+
if (Array.isArray(schema.items)) {
|
|
1028
|
+
for (let i = 0; i < schema.items.length; i++) {
|
|
1029
|
+
traverse(itemsLocation.getPropertyLocation(i))
|
|
1030
|
+
}
|
|
1031
|
+
} else {
|
|
1032
|
+
traverse(itemsLocation)
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (schema.additionalItems && typeof schema.additionalItems === 'object') {
|
|
1036
|
+
traverse(location.getPropertyLocation('additionalItems'))
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (schema.oneOf) {
|
|
1040
|
+
const oneOfLocation = location.getPropertyLocation('oneOf')
|
|
1041
|
+
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
1042
|
+
traverse(oneOfLocation.getPropertyLocation(i))
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (schema.anyOf) {
|
|
1046
|
+
const anyOfLocation = location.getPropertyLocation('anyOf')
|
|
1047
|
+
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
1048
|
+
traverse(anyOfLocation.getPropertyLocation(i))
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (schema.allOf) {
|
|
1052
|
+
const allOfLocation = location.getPropertyLocation('allOf')
|
|
1053
|
+
for (let i = 0; i < schema.allOf.length; i++) {
|
|
1054
|
+
traverse(allOfLocation.getPropertyLocation(i))
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (schema.then) traverse(location.getPropertyLocation('then'))
|
|
1058
|
+
if (schema.else) traverse(location.getPropertyLocation('else'))
|
|
1059
|
+
|
|
1060
|
+
pathStack.delete(fullPath)
|
|
1061
|
+
}
|
|
1062
|
+
traverse(location)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
796
1065
|
function buildConstSerializer (location, input) {
|
|
797
1066
|
const schema = location.schema
|
|
798
1067
|
const type = schema.type
|
|
@@ -842,7 +1111,7 @@ function buildAllOf (context, location, input) {
|
|
|
842
1111
|
]
|
|
843
1112
|
|
|
844
1113
|
const allOfsLocation = location.getPropertyLocation('allOf')
|
|
845
|
-
for (let i = 0
|
|
1114
|
+
for (let i = 0, allOfLength = allOf.length; i < allOfLength; i++) {
|
|
846
1115
|
locations.push(allOfsLocation.getPropertyLocation(i))
|
|
847
1116
|
}
|
|
848
1117
|
|
|
@@ -867,7 +1136,7 @@ function buildOneOf (context, location, input) {
|
|
|
867
1136
|
|
|
868
1137
|
let code = ''
|
|
869
1138
|
|
|
870
|
-
for (let index = 0
|
|
1139
|
+
for (let index = 0, oneOfsLength = oneOfs.length; index < oneOfsLength; index++) {
|
|
871
1140
|
const optionLocation = oneOfsLocation.getPropertyLocation(index)
|
|
872
1141
|
const optionSchema = optionLocation.schema
|
|
873
1142
|
|
|
@@ -887,19 +1156,16 @@ function buildOneOf (context, location, input) {
|
|
|
887
1156
|
|
|
888
1157
|
const nestedResult = buildValue(context, mergedLocation, input)
|
|
889
1158
|
const schemaRef = optionLocation.getSchemaRef()
|
|
1159
|
+
|
|
890
1160
|
code += `
|
|
891
|
-
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
|
|
1161
|
+
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) {
|
|
892
1162
|
${nestedResult}
|
|
1163
|
+
}
|
|
893
1164
|
`
|
|
894
1165
|
}
|
|
895
1166
|
|
|
896
|
-
let schemaRef = location.getSchemaRef()
|
|
897
|
-
if (schemaRef.startsWith(context.rootSchemaId)) {
|
|
898
|
-
schemaRef = schemaRef.replace(context.rootSchemaId, '')
|
|
899
|
-
}
|
|
900
|
-
|
|
901
1167
|
code += `
|
|
902
|
-
else throw new TypeError(\`The value of '${
|
|
1168
|
+
else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
|
|
903
1169
|
`
|
|
904
1170
|
|
|
905
1171
|
return code
|
package/lib/validator.js
CHANGED
|
@@ -21,8 +21,8 @@ class Validator {
|
|
|
21
21
|
keyword: 'fjs_type',
|
|
22
22
|
type: 'object',
|
|
23
23
|
errors: false,
|
|
24
|
-
validate: (_type,
|
|
25
|
-
return
|
|
24
|
+
validate: (_type, data) => {
|
|
25
|
+
return data && typeof data.toJSON === 'function'
|
|
26
26
|
}
|
|
27
27
|
})
|
|
28
28
|
|
|
@@ -51,8 +51,10 @@ class Validator {
|
|
|
51
51
|
return this.ajv.validate(schemaRef, data)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Ajv does not support
|
|
55
|
-
//
|
|
54
|
+
// Ajv does not natively support JavaScript objects like Date or other types
|
|
55
|
+
// that rely on a custom .toJSON() representation. To properly validate schemas
|
|
56
|
+
// that may contain such objects (e.g. Date, ObjectId, etc.), we replace all
|
|
57
|
+
// occurrences of the string type with a custom keyword fjs_type
|
|
56
58
|
// (see https://github.com/fastify/fast-json-stringify/pull/441)
|
|
57
59
|
convertSchemaToAjvFormat (schema) {
|
|
58
60
|
if (schema === null) return
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-json-stringify",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "Stringify your JSON at max speed",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"types": "types/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"bench": "node ./benchmark/bench.js",
|
|
9
|
+
"bench": "node --expose-gc ./benchmark/bench.js",
|
|
10
10
|
"bench:cmp": "node ./benchmark/bench-cmp-branch.js",
|
|
11
11
|
"bench:cmp:ci": "node ./benchmark/bench-cmp-branch.js --ci",
|
|
12
|
-
"benchmark": "node ./benchmark/bench-cmp-lib.js",
|
|
12
|
+
"benchmark": "node --expose-gc ./benchmark/bench-cmp-lib.js",
|
|
13
13
|
"lint": "eslint",
|
|
14
14
|
"lint:fix": "eslint --fix",
|
|
15
15
|
"test:typescript": "tsd",
|
|
@@ -62,19 +62,20 @@
|
|
|
62
62
|
}
|
|
63
63
|
],
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@sinclair/typebox": "^0.34.
|
|
66
|
-
"
|
|
67
|
-
"c8": "^10.1.2",
|
|
65
|
+
"@sinclair/typebox": "^0.34.48",
|
|
66
|
+
"c8": "^10.1.3",
|
|
68
67
|
"cli-select": "^1.1.2",
|
|
69
68
|
"compile-json-stringify": "^0.1.2",
|
|
70
|
-
"eslint": "^9.
|
|
69
|
+
"eslint": "^9.39.2",
|
|
71
70
|
"fast-json-stringify": ".",
|
|
72
71
|
"is-my-json-valid": "^2.20.6",
|
|
73
|
-
"json-accelerator": "^0.
|
|
74
|
-
"neostandard": "^0.12.
|
|
75
|
-
"simple-git": "^3.
|
|
76
|
-
"
|
|
77
|
-
"
|
|
72
|
+
"json-accelerator": "^0.1.7",
|
|
73
|
+
"neostandard": "^0.12.2",
|
|
74
|
+
"simple-git": "^3.30.0",
|
|
75
|
+
"tinybench": "^6.0.0",
|
|
76
|
+
"tsd": "^0.33.0",
|
|
77
|
+
"typescript": "^5.9.3",
|
|
78
|
+
"webpack": "^5.104.1"
|
|
78
79
|
},
|
|
79
80
|
"dependencies": {
|
|
80
81
|
"@fastify/merge-json-schemas": "^0.2.0",
|
package/test/array.test.js
CHANGED
|
@@ -61,6 +61,34 @@ buildTest({
|
|
|
61
61
|
dates: [new Date(1), new Date(2)]
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
+
buildTest({
|
|
65
|
+
title: 'dates tuple $ref',
|
|
66
|
+
definitions: {
|
|
67
|
+
dateTime: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
format: 'date-time'
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
dates: {
|
|
75
|
+
type: 'array',
|
|
76
|
+
minItems: 2,
|
|
77
|
+
maxItems: 2,
|
|
78
|
+
items: [
|
|
79
|
+
{
|
|
80
|
+
$ref: '#/definitions/dateTime'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
$ref: '#/definitions/dateTime'
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}, {
|
|
89
|
+
dates: [new Date(1), new Date(2)]
|
|
90
|
+
})
|
|
91
|
+
|
|
64
92
|
buildTest({
|
|
65
93
|
title: 'string array',
|
|
66
94
|
type: 'object',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
test('additionalProperties: false', (t) => {
|
|
7
|
+
t.plan(1)
|
|
8
|
+
const stringify = build({
|
|
9
|
+
title: 'additionalProperties',
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
foo: {
|
|
13
|
+
type: 'string'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
additionalProperties: false
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const obj = { foo: 'a', bar: 'b', baz: 'c' }
|
|
20
|
+
t.assert.equal(stringify(obj), '{"foo":"a"}')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('additionalProperties: {}', (t) => {
|
|
24
|
+
t.plan(1)
|
|
25
|
+
const stringify = build({
|
|
26
|
+
title: 'additionalProperties',
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
foo: {
|
|
30
|
+
type: 'string'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
additionalProperties: {}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const obj = { foo: 'a', bar: 'b', baz: 'c' }
|
|
37
|
+
t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('additionalProperties: {type: string}', (t) => {
|
|
41
|
+
t.plan(1)
|
|
42
|
+
const stringify = build({
|
|
43
|
+
title: 'additionalProperties',
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
foo: {
|
|
47
|
+
type: 'string'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
additionalProperties: {
|
|
51
|
+
type: 'string'
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const obj = { foo: 'a', bar: 'b', baz: 'c' }
|
|
56
|
+
t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
|
|
57
|
+
})
|