fast-json-stringify 6.1.1 → 6.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/workflows/ci.yml +5 -0
- package/README.md +0 -1
- package/benchmark/bench-cmp-lib.js +21 -7
- package/benchmark/bench-thread.js +24 -12
- package/index.js +330 -132
- package/lib/validator.js +6 -4
- package/package.json +4 -4
- 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
|
|
@@ -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
|
|
@@ -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.hz // ops/sec
|
|
314
|
+
const rme = task.result.rme // relative margin of error (%)
|
|
315
|
+
const samples = task.result.samples.length
|
|
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.hz // ops/sec
|
|
25
|
+
const rme = task.result.rme // relative margin of error (%)
|
|
26
|
+
const samples = task.result.samples.length
|
|
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,13 +299,13 @@ 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
307
|
const propertiesKeys = ${JSON.stringify(propertiesKeys)}
|
|
292
|
-
for (const [key, value] of Object.entries(
|
|
308
|
+
for (const [key, value] of Object.entries(${objVar})) {
|
|
293
309
|
if (
|
|
294
310
|
propertiesKeys.includes(key) ||
|
|
295
311
|
value === undefined ||
|
|
@@ -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,23 +371,25 @@ 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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
388
|
+
const needsRuntimeComma = propertiesKeys.length > 1 || schema.patternProperties || (schema.additionalProperties !== undefined && schema.additionalProperties !== false)
|
|
389
|
+
|
|
390
|
+
if (needsRuntimeComma) {
|
|
391
|
+
code += `let addComma_${localUid} = false\n`
|
|
392
|
+
addComma = `!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)`
|
|
375
393
|
}
|
|
376
394
|
|
|
377
395
|
for (const key of propertiesKeys) {
|
|
@@ -381,15 +399,16 @@ function buildInnerObject (context, location) {
|
|
|
381
399
|
}
|
|
382
400
|
|
|
383
401
|
const sanitizedKey = JSON.stringify(key)
|
|
402
|
+
const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
|
|
384
403
|
const defaultValue = propertyLocation.schema.default
|
|
385
404
|
const isRequired = requiredProperties.includes(key)
|
|
386
405
|
|
|
387
406
|
code += `
|
|
388
|
-
value =
|
|
389
|
-
if (value !== undefined) {
|
|
407
|
+
const ${value} = ${objVar}[${sanitizedKey}]
|
|
408
|
+
if (${value} !== undefined) {
|
|
390
409
|
${addComma}
|
|
391
410
|
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
392
|
-
${buildValue(context, propertyLocation,
|
|
411
|
+
${buildValue(context, propertyLocation, `${value}`)}
|
|
393
412
|
}`
|
|
394
413
|
|
|
395
414
|
if (defaultValue !== undefined) {
|
|
@@ -406,24 +425,20 @@ function buildInnerObject (context, location) {
|
|
|
406
425
|
} else {
|
|
407
426
|
code += '\n'
|
|
408
427
|
}
|
|
409
|
-
|
|
410
|
-
if (hasRequiredProperties) {
|
|
411
|
-
addComma = 'json += \',\''
|
|
412
|
-
}
|
|
413
428
|
}
|
|
414
429
|
|
|
415
430
|
if (schema.patternProperties || schema.additionalProperties) {
|
|
416
|
-
code += buildExtraObjectPropertiesSerializer(context, location, addComma)
|
|
431
|
+
code += buildExtraObjectPropertiesSerializer(context, location, addComma, objVar)
|
|
417
432
|
}
|
|
418
433
|
|
|
419
434
|
code += `
|
|
420
|
-
|
|
435
|
+
json += JSON_STR_END_OBJECT
|
|
421
436
|
`
|
|
422
437
|
return code
|
|
423
438
|
}
|
|
424
439
|
|
|
425
440
|
function mergeLocations (context, mergedSchemaId, mergedLocations) {
|
|
426
|
-
for (let i = 0
|
|
441
|
+
for (let i = 0, mergedLocationsLength = mergedLocations.length; i < mergedLocationsLength; i++) {
|
|
427
442
|
const location = mergedLocations[i]
|
|
428
443
|
const schema = location.schema
|
|
429
444
|
if (schema.$ref) {
|
|
@@ -485,40 +500,57 @@ function toJSON (variableName) {
|
|
|
485
500
|
`
|
|
486
501
|
}
|
|
487
502
|
|
|
488
|
-
function buildObject (context, location) {
|
|
503
|
+
function buildObject (context, location, input) {
|
|
489
504
|
const schema = location.schema
|
|
490
505
|
|
|
491
506
|
if (context.functionsNamesBySchema.has(schema)) {
|
|
492
|
-
|
|
507
|
+
const funcName = context.functionsNamesBySchema.get(schema)
|
|
508
|
+
return `json += ${funcName}(${input})`
|
|
493
509
|
}
|
|
494
510
|
|
|
495
|
-
const
|
|
496
|
-
context.functionsNamesBySchema.set(schema, functionName)
|
|
511
|
+
const nullable = schema.nullable === true
|
|
497
512
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
513
|
+
const schemaId = location.schemaId || ''
|
|
514
|
+
const jsonPointer = location.jsonPointer || ''
|
|
515
|
+
const fullPath = `${schemaId}#${jsonPointer}`
|
|
502
516
|
|
|
503
|
-
|
|
504
|
-
|
|
517
|
+
if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
|
|
518
|
+
const functionName = generateFuncName(context)
|
|
519
|
+
context.functionsNamesBySchema.set(schema, functionName)
|
|
505
520
|
|
|
506
|
-
|
|
507
|
-
functionCode += `
|
|
508
|
-
// ${schemaRef}
|
|
509
|
-
function ${functionName} (input) {
|
|
510
|
-
const obj = ${toJSON('input')}
|
|
511
|
-
${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_OBJECT' : ''}
|
|
521
|
+
const schemaRef = getSafeSchemaRef(context, location)
|
|
512
522
|
|
|
513
|
-
|
|
523
|
+
const functionCode = `
|
|
524
|
+
// ${schemaRef}
|
|
525
|
+
function ${functionName} (input) {
|
|
526
|
+
const obj = ${toJSON('input')}
|
|
527
|
+
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
|
|
528
|
+
let json = ''
|
|
529
|
+
|
|
530
|
+
${buildInnerObject(context, location, 'obj')}
|
|
531
|
+
return json
|
|
532
|
+
}
|
|
533
|
+
`
|
|
534
|
+
|
|
535
|
+
context.functions.push(functionCode)
|
|
536
|
+
return `json += ${functionName}(${input})`
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
context.buildingSet.add(schema)
|
|
540
|
+
const objVar = `obj_${context.uid++}`
|
|
541
|
+
const code = `
|
|
542
|
+
const ${objVar} = ${toJSON(input)}
|
|
543
|
+
if (${objVar} === null) {
|
|
544
|
+
json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
|
|
545
|
+
} else {
|
|
546
|
+
${buildInnerObject(context, location, objVar)}
|
|
514
547
|
}
|
|
515
548
|
`
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return functionName
|
|
549
|
+
context.buildingSet.delete(schema)
|
|
550
|
+
return code
|
|
519
551
|
}
|
|
520
552
|
|
|
521
|
-
function buildArray (context, location) {
|
|
553
|
+
function buildArray (context, location, input) {
|
|
522
554
|
const schema = location.schema
|
|
523
555
|
|
|
524
556
|
let itemsLocation = location.getPropertyLocation('items')
|
|
@@ -531,61 +563,143 @@ function buildArray (context, location) {
|
|
|
531
563
|
const itemsSchema = itemsLocation.schema
|
|
532
564
|
|
|
533
565
|
if (context.functionsNamesBySchema.has(schema)) {
|
|
534
|
-
|
|
566
|
+
const funcName = context.functionsNamesBySchema.get(schema)
|
|
567
|
+
return `json += ${funcName}(${input})`
|
|
535
568
|
}
|
|
536
569
|
|
|
537
|
-
const
|
|
538
|
-
context.functionsNamesBySchema.set(schema, functionName)
|
|
570
|
+
const nullable = schema.nullable === true
|
|
539
571
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
572
|
+
const schemaId = location.schemaId || ''
|
|
573
|
+
const jsonPointer = location.jsonPointer || ''
|
|
574
|
+
const fullPath = `${schemaId}#${jsonPointer}`
|
|
575
|
+
|
|
576
|
+
if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
|
|
577
|
+
const functionName = generateFuncName(context)
|
|
578
|
+
context.functionsNamesBySchema.set(schema, functionName)
|
|
544
579
|
|
|
545
|
-
|
|
580
|
+
const schemaRef = getSafeSchemaRef(context, location)
|
|
581
|
+
|
|
582
|
+
let functionCode = `
|
|
546
583
|
function ${functionName} (obj) {
|
|
547
584
|
// ${schemaRef}
|
|
585
|
+
let json = ''
|
|
548
586
|
`
|
|
549
587
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_ARRAY' : ''}
|
|
588
|
+
functionCode += `
|
|
589
|
+
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
|
|
553
590
|
if (!Array.isArray(obj)) {
|
|
554
591
|
throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
|
|
555
592
|
}
|
|
556
593
|
const arrayLength = obj.length
|
|
557
594
|
`
|
|
558
595
|
|
|
559
|
-
|
|
560
|
-
|
|
596
|
+
if (!schema.additionalItems && Array.isArray(itemsSchema)) {
|
|
597
|
+
functionCode += `
|
|
561
598
|
if (arrayLength > ${itemsSchema.length}) {
|
|
562
599
|
throw new Error(\`Item at ${itemsSchema.length} does not match schema definition.\`)
|
|
563
600
|
}
|
|
564
601
|
`
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (largeArrayMechanism === 'json-stringify') {
|
|
605
|
+
functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
functionCode += `
|
|
609
|
+
json += JSON_STR_BEGIN_ARRAY
|
|
610
|
+
`
|
|
611
|
+
|
|
612
|
+
if (Array.isArray(itemsSchema)) {
|
|
613
|
+
for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) {
|
|
614
|
+
const item = itemsSchema[i]
|
|
615
|
+
const value = `value_${i}`
|
|
616
|
+
functionCode += `const ${value} = obj[${i}]`
|
|
617
|
+
const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), value)
|
|
618
|
+
functionCode += `
|
|
619
|
+
if (${i} < arrayLength) {
|
|
620
|
+
if (${buildArrayTypeCondition(item.type, value)}) {
|
|
621
|
+
if (${i}) {
|
|
622
|
+
json += JSON_STR_COMMA
|
|
623
|
+
}
|
|
624
|
+
${tmpRes}
|
|
625
|
+
} else {
|
|
626
|
+
throw new Error(\`Item at ${i} does not match schema definition.\`)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
`
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (schema.additionalItems) {
|
|
633
|
+
functionCode += `
|
|
634
|
+
for (let i = ${itemsSchema.length}; i < arrayLength; i++) {
|
|
635
|
+
if (i) {
|
|
636
|
+
json += JSON_STR_COMMA
|
|
637
|
+
}
|
|
638
|
+
json += JSON.stringify(obj[i])
|
|
639
|
+
}`
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
const code = buildValue(context, itemsLocation, 'value')
|
|
643
|
+
functionCode += `
|
|
644
|
+
for (let i = 0; i < arrayLength; i++) {
|
|
645
|
+
if (i) {
|
|
646
|
+
json += JSON_STR_COMMA
|
|
647
|
+
}
|
|
648
|
+
const value = obj[i]
|
|
649
|
+
${code}
|
|
650
|
+
}`
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
functionCode += `
|
|
654
|
+
return json + JSON_STR_END_ARRAY
|
|
655
|
+
}`
|
|
656
|
+
|
|
657
|
+
context.functions.push(functionCode)
|
|
658
|
+
return `json += ${functionName}(${input})`
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
context.buildingSet.add(schema)
|
|
662
|
+
const safeSchemaRef = getSafeSchemaRef(context, location)
|
|
663
|
+
const objVar = `obj_${context.uid++}`
|
|
664
|
+
let inlinedCode = `
|
|
665
|
+
const ${objVar} = ${input}
|
|
666
|
+
if (${objVar} === null) {
|
|
667
|
+
json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
|
|
668
|
+
} else if (!Array.isArray(${objVar})) {
|
|
669
|
+
throw new TypeError(\`The value of '${safeSchemaRef}' does not match schema definition.\`)
|
|
670
|
+
} else {
|
|
671
|
+
const arrayLength_${objVar} = ${objVar}.length
|
|
672
|
+
`
|
|
673
|
+
|
|
674
|
+
if (!schema.additionalItems && Array.isArray(itemsSchema)) {
|
|
675
|
+
inlinedCode += `
|
|
676
|
+
if (arrayLength_${objVar} > ${itemsSchema.length}) {
|
|
677
|
+
throw new Error(\`Item at ${itemsSchema.length} does not match schema definition.\`)
|
|
678
|
+
}
|
|
679
|
+
`
|
|
565
680
|
}
|
|
566
681
|
|
|
567
682
|
if (largeArrayMechanism === 'json-stringify') {
|
|
568
|
-
|
|
683
|
+
inlinedCode += `if (arrayLength_${objVar} >= ${largeArraySize}) json += JSON.stringify(${objVar})\n else {`
|
|
569
684
|
}
|
|
570
685
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
let value
|
|
574
|
-
let json = ''
|
|
686
|
+
inlinedCode += `
|
|
687
|
+
json += JSON_STR_BEGIN_ARRAY
|
|
575
688
|
`
|
|
576
689
|
|
|
577
690
|
if (Array.isArray(itemsSchema)) {
|
|
578
|
-
|
|
691
|
+
const localUid = context.uid++
|
|
692
|
+
inlinedCode += `let addComma_${localUid} = false\n`
|
|
693
|
+
for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) {
|
|
579
694
|
const item = itemsSchema[i]
|
|
580
|
-
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
695
|
+
const value = `value_${i}_${context.uid++}`
|
|
696
|
+
inlinedCode += `const ${value} = ${objVar}[${i}]`
|
|
697
|
+
const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), value)
|
|
698
|
+
inlinedCode += `
|
|
699
|
+
if (${i} < arrayLength_${objVar}) {
|
|
700
|
+
if (${buildArrayTypeCondition(item.type, value)}) {
|
|
701
|
+
!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)
|
|
585
702
|
${tmpRes}
|
|
586
|
-
if (${i} < arrayEnd) {
|
|
587
|
-
json += JSON_STR_COMMA
|
|
588
|
-
}
|
|
589
703
|
} else {
|
|
590
704
|
throw new Error(\`Item at ${i} does not match schema definition.\`)
|
|
591
705
|
}
|
|
@@ -594,66 +708,68 @@ function buildArray (context, location) {
|
|
|
594
708
|
}
|
|
595
709
|
|
|
596
710
|
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
|
-
}
|
|
711
|
+
inlinedCode += `
|
|
712
|
+
for (let i = ${itemsSchema.length}; i < arrayLength_${objVar}; i++) {
|
|
713
|
+
!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)
|
|
714
|
+
json += JSON.stringify(${objVar}[i])
|
|
604
715
|
}`
|
|
605
716
|
}
|
|
606
717
|
} else {
|
|
607
718
|
const code = buildValue(context, itemsLocation, 'value')
|
|
608
|
-
|
|
609
|
-
for (let i = 0; i <
|
|
610
|
-
|
|
611
|
-
${code}
|
|
612
|
-
if (i < arrayEnd) {
|
|
719
|
+
inlinedCode += `
|
|
720
|
+
for (let i = 0; i < arrayLength_${objVar}; i++) {
|
|
721
|
+
if (i) {
|
|
613
722
|
json += JSON_STR_COMMA
|
|
614
723
|
}
|
|
724
|
+
const value = ${objVar}[i]
|
|
725
|
+
${code}
|
|
615
726
|
}`
|
|
616
727
|
}
|
|
617
728
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
729
|
+
inlinedCode += `
|
|
730
|
+
json += JSON_STR_END_ARRAY
|
|
731
|
+
`
|
|
621
732
|
|
|
622
|
-
|
|
623
|
-
|
|
733
|
+
if (largeArrayMechanism === 'json-stringify') {
|
|
734
|
+
inlinedCode += '}'
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
inlinedCode += '}'
|
|
738
|
+
context.buildingSet.delete(schema)
|
|
739
|
+
return inlinedCode
|
|
624
740
|
}
|
|
625
741
|
|
|
626
742
|
function buildArrayTypeCondition (type, accessor) {
|
|
627
743
|
let condition
|
|
628
744
|
switch (type) {
|
|
629
745
|
case 'null':
|
|
630
|
-
condition =
|
|
746
|
+
condition = `${accessor} === null`
|
|
631
747
|
break
|
|
632
748
|
case 'string':
|
|
633
|
-
condition = `typeof
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
749
|
+
condition = `typeof ${accessor} === 'string' ||
|
|
750
|
+
${accessor} === null ||
|
|
751
|
+
${accessor} instanceof Date ||
|
|
752
|
+
${accessor} instanceof RegExp ||
|
|
637
753
|
(
|
|
638
|
-
typeof
|
|
639
|
-
typeof
|
|
640
|
-
|
|
754
|
+
typeof ${accessor} === "object" &&
|
|
755
|
+
typeof ${accessor}.toString === "function" &&
|
|
756
|
+
${accessor}.toString !== Object.prototype.toString
|
|
641
757
|
)`
|
|
642
758
|
break
|
|
643
759
|
case 'integer':
|
|
644
|
-
condition =
|
|
760
|
+
condition = `Number.isInteger(${accessor})`
|
|
645
761
|
break
|
|
646
762
|
case 'number':
|
|
647
|
-
condition =
|
|
763
|
+
condition = `Number.isFinite(${accessor})`
|
|
648
764
|
break
|
|
649
765
|
case 'boolean':
|
|
650
|
-
condition =
|
|
766
|
+
condition = `typeof ${accessor} === 'boolean'`
|
|
651
767
|
break
|
|
652
768
|
case 'object':
|
|
653
|
-
condition =
|
|
769
|
+
condition = `${accessor} && typeof ${accessor} === 'object' && ${accessor}.constructor === Object`
|
|
654
770
|
break
|
|
655
771
|
case 'array':
|
|
656
|
-
condition =
|
|
772
|
+
condition = `Array.isArray(${accessor})`
|
|
657
773
|
break
|
|
658
774
|
default:
|
|
659
775
|
if (Array.isArray(type)) {
|
|
@@ -684,8 +800,9 @@ function buildMultiTypeSerializer (context, location, input) {
|
|
|
684
800
|
switch (type) {
|
|
685
801
|
case 'null':
|
|
686
802
|
code += `
|
|
687
|
-
${statement} (${input} === null)
|
|
803
|
+
${statement} (${input} === null) {
|
|
688
804
|
${nestedResult}
|
|
805
|
+
}
|
|
689
806
|
`
|
|
690
807
|
break
|
|
691
808
|
case 'string': {
|
|
@@ -700,40 +817,40 @@ function buildMultiTypeSerializer (context, location, input) {
|
|
|
700
817
|
typeof ${input}.toString === "function" &&
|
|
701
818
|
${input}.toString !== Object.prototype.toString
|
|
702
819
|
)
|
|
703
|
-
)
|
|
820
|
+
) {
|
|
704
821
|
${nestedResult}
|
|
822
|
+
}
|
|
705
823
|
`
|
|
706
824
|
break
|
|
707
825
|
}
|
|
708
826
|
case 'array': {
|
|
709
827
|
code += `
|
|
710
|
-
${statement}(Array.isArray(${input}))
|
|
828
|
+
${statement}(Array.isArray(${input})) {
|
|
711
829
|
${nestedResult}
|
|
830
|
+
}
|
|
712
831
|
`
|
|
713
832
|
break
|
|
714
833
|
}
|
|
715
834
|
case 'integer': {
|
|
716
835
|
code += `
|
|
717
|
-
${statement}(Number.isInteger(${input}) || ${input} === null)
|
|
836
|
+
${statement}(Number.isInteger(${input}) || ${input} === null) {
|
|
718
837
|
${nestedResult}
|
|
838
|
+
}
|
|
719
839
|
`
|
|
720
840
|
break
|
|
721
841
|
}
|
|
722
842
|
default: {
|
|
723
843
|
code += `
|
|
724
|
-
${statement}(typeof ${input} === "${type}" || ${input} === null)
|
|
844
|
+
${statement}(typeof ${input} === "${type}" || ${input} === null) {
|
|
725
845
|
${nestedResult}
|
|
846
|
+
}
|
|
726
847
|
`
|
|
727
848
|
break
|
|
728
849
|
}
|
|
729
850
|
}
|
|
730
851
|
})
|
|
731
|
-
let schemaRef = location.getSchemaRef()
|
|
732
|
-
if (schemaRef.startsWith(context.rootSchemaId)) {
|
|
733
|
-
schemaRef = schemaRef.replace(context.rootSchemaId, '')
|
|
734
|
-
}
|
|
735
852
|
code += `
|
|
736
|
-
else throw new TypeError(\`The value of '${
|
|
853
|
+
else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
|
|
737
854
|
`
|
|
738
855
|
|
|
739
856
|
return code
|
|
@@ -779,12 +896,10 @@ function buildSingleTypeSerializer (context, location, input) {
|
|
|
779
896
|
case 'boolean':
|
|
780
897
|
return `json += asBoolean(${input})`
|
|
781
898
|
case 'object': {
|
|
782
|
-
|
|
783
|
-
return `json += ${funcName}(${input})`
|
|
899
|
+
return buildObject(context, location, input)
|
|
784
900
|
}
|
|
785
901
|
case 'array': {
|
|
786
|
-
|
|
787
|
-
return `json += ${funcName}(${input})`
|
|
902
|
+
return buildArray(context, location, input)
|
|
788
903
|
}
|
|
789
904
|
case undefined:
|
|
790
905
|
return `json += JSON.stringify(${input})`
|
|
@@ -793,6 +908,92 @@ function buildSingleTypeSerializer (context, location, input) {
|
|
|
793
908
|
}
|
|
794
909
|
}
|
|
795
910
|
|
|
911
|
+
function detectRecursiveSchemas (context, location) {
|
|
912
|
+
const pathStack = new Set()
|
|
913
|
+
function traverse (location) {
|
|
914
|
+
const schema = location.schema
|
|
915
|
+
if (typeof schema !== 'object' || schema === null) return
|
|
916
|
+
|
|
917
|
+
const schemaId = location.schemaId || ''
|
|
918
|
+
const jsonPointer = location.jsonPointer || ''
|
|
919
|
+
const fullPath = `${schemaId}#${jsonPointer}`
|
|
920
|
+
|
|
921
|
+
if (pathStack.has(fullPath)) {
|
|
922
|
+
// Mark all nodes in the current path that are part of the cycle
|
|
923
|
+
let inCycle = false
|
|
924
|
+
for (const p of pathStack) {
|
|
925
|
+
if (p === fullPath) inCycle = true
|
|
926
|
+
if (inCycle) context.recursivePaths.add(p)
|
|
927
|
+
}
|
|
928
|
+
context.recursivePaths.add(fullPath)
|
|
929
|
+
return
|
|
930
|
+
}
|
|
931
|
+
pathStack.add(fullPath)
|
|
932
|
+
|
|
933
|
+
if (schema.$ref) {
|
|
934
|
+
try {
|
|
935
|
+
const res = resolveRef(context, location)
|
|
936
|
+
traverse(res)
|
|
937
|
+
} catch (err) {
|
|
938
|
+
// Validation will handle missing refs later
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (schema.properties) {
|
|
943
|
+
const propertiesLocation = location.getPropertyLocation('properties')
|
|
944
|
+
for (const key in schema.properties) {
|
|
945
|
+
traverse(propertiesLocation.getPropertyLocation(key))
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
949
|
+
traverse(location.getPropertyLocation('additionalProperties'))
|
|
950
|
+
}
|
|
951
|
+
if (schema.patternProperties) {
|
|
952
|
+
const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
|
|
953
|
+
for (const key in schema.patternProperties) {
|
|
954
|
+
traverse(patternPropertiesLocation.getPropertyLocation(key))
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (schema.items) {
|
|
958
|
+
const itemsLocation = location.getPropertyLocation('items')
|
|
959
|
+
if (Array.isArray(schema.items)) {
|
|
960
|
+
for (let i = 0; i < schema.items.length; i++) {
|
|
961
|
+
traverse(itemsLocation.getPropertyLocation(i))
|
|
962
|
+
}
|
|
963
|
+
} else {
|
|
964
|
+
traverse(itemsLocation)
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (schema.additionalItems && typeof schema.additionalItems === 'object') {
|
|
968
|
+
traverse(location.getPropertyLocation('additionalItems'))
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (schema.oneOf) {
|
|
972
|
+
const oneOfLocation = location.getPropertyLocation('oneOf')
|
|
973
|
+
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
974
|
+
traverse(oneOfLocation.getPropertyLocation(i))
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (schema.anyOf) {
|
|
978
|
+
const anyOfLocation = location.getPropertyLocation('anyOf')
|
|
979
|
+
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
980
|
+
traverse(anyOfLocation.getPropertyLocation(i))
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (schema.allOf) {
|
|
984
|
+
const allOfLocation = location.getPropertyLocation('allOf')
|
|
985
|
+
for (let i = 0; i < schema.allOf.length; i++) {
|
|
986
|
+
traverse(allOfLocation.getPropertyLocation(i))
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (schema.then) traverse(location.getPropertyLocation('then'))
|
|
990
|
+
if (schema.else) traverse(location.getPropertyLocation('else'))
|
|
991
|
+
|
|
992
|
+
pathStack.delete(fullPath)
|
|
993
|
+
}
|
|
994
|
+
traverse(location)
|
|
995
|
+
}
|
|
996
|
+
|
|
796
997
|
function buildConstSerializer (location, input) {
|
|
797
998
|
const schema = location.schema
|
|
798
999
|
const type = schema.type
|
|
@@ -842,7 +1043,7 @@ function buildAllOf (context, location, input) {
|
|
|
842
1043
|
]
|
|
843
1044
|
|
|
844
1045
|
const allOfsLocation = location.getPropertyLocation('allOf')
|
|
845
|
-
for (let i = 0
|
|
1046
|
+
for (let i = 0, allOfLength = allOf.length; i < allOfLength; i++) {
|
|
846
1047
|
locations.push(allOfsLocation.getPropertyLocation(i))
|
|
847
1048
|
}
|
|
848
1049
|
|
|
@@ -867,7 +1068,7 @@ function buildOneOf (context, location, input) {
|
|
|
867
1068
|
|
|
868
1069
|
let code = ''
|
|
869
1070
|
|
|
870
|
-
for (let index = 0
|
|
1071
|
+
for (let index = 0, oneOfsLength = oneOfs.length; index < oneOfsLength; index++) {
|
|
871
1072
|
const optionLocation = oneOfsLocation.getPropertyLocation(index)
|
|
872
1073
|
const optionSchema = optionLocation.schema
|
|
873
1074
|
|
|
@@ -887,19 +1088,16 @@ function buildOneOf (context, location, input) {
|
|
|
887
1088
|
|
|
888
1089
|
const nestedResult = buildValue(context, mergedLocation, input)
|
|
889
1090
|
const schemaRef = optionLocation.getSchemaRef()
|
|
1091
|
+
|
|
890
1092
|
code += `
|
|
891
|
-
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
|
|
1093
|
+
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) {
|
|
892
1094
|
${nestedResult}
|
|
1095
|
+
}
|
|
893
1096
|
`
|
|
894
1097
|
}
|
|
895
1098
|
|
|
896
|
-
let schemaRef = location.getSchemaRef()
|
|
897
|
-
if (schemaRef.startsWith(context.rootSchemaId)) {
|
|
898
|
-
schemaRef = schemaRef.replace(context.rootSchemaId, '')
|
|
899
|
-
}
|
|
900
|
-
|
|
901
1099
|
code += `
|
|
902
|
-
else throw new TypeError(\`The value of '${
|
|
1100
|
+
else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
|
|
903
1101
|
`
|
|
904
1102
|
|
|
905
1103
|
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.2.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",
|
|
@@ -63,7 +63,6 @@
|
|
|
63
63
|
],
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@sinclair/typebox": "^0.34.3",
|
|
66
|
-
"benchmark": "^2.1.4",
|
|
67
66
|
"c8": "^10.1.2",
|
|
68
67
|
"cli-select": "^1.1.2",
|
|
69
68
|
"compile-json-stringify": "^0.1.2",
|
|
@@ -73,6 +72,7 @@
|
|
|
73
72
|
"json-accelerator": "^0.0.2",
|
|
74
73
|
"neostandard": "^0.12.0",
|
|
75
74
|
"simple-git": "^3.23.0",
|
|
75
|
+
"tinybench": "^5.0.1",
|
|
76
76
|
"tsd": "^0.32.0",
|
|
77
77
|
"webpack": "^5.90.3"
|
|
78
78
|
},
|
|
@@ -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
|
+
})
|