fast-json-stringify 6.1.0 → 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.
@@ -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 benchmark = require('benchmark')
4
- const suite = new benchmark.Suite()
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.on('cycle', cycle)
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
- suite.run()
317
+ const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 })
318
+ const formattedRme = rme.toFixed(2)
306
319
 
307
- function cycle (e) {
308
- console.log(e.target.toString())
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 Benchmark = require('benchmark')
6
- Benchmark.options.minSamples = 100
5
+ const { Bench } = require('tinybench')
7
6
 
8
- const suite = Benchmark.Suite()
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
- suite
14
- .add(benchmark.name, () => {
15
- stringify(benchmark.input)
16
- })
17
- .on('cycle', (event) => {
18
- parentPort.postMessage(String(event.target))
19
- })
20
- .on('complete', () => {})
21
- .run()
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.includes(options.rounding)) {
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.includes(options.largeArrayMechanism)) {
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
- if (typeof options.largeArraySize === 'string' && Number.isFinite(Number.parseInt(options.largeArraySize, 10))) {
145
- largeArraySize = Number.parseInt(options.largeArraySize, 10)
146
- } else if (typeof options.largeArraySize === 'number' && Number.isInteger(options.largeArraySize)) {
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 (typeof options.largeArraySize === 'bigint') {
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(obj)) {
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 = 'let value\n'
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 (obj[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')\n`
380
+ code += `if (${objVar}[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')\n`
366
381
  }
367
382
  }
368
383
 
369
- code += 'let json = JSON_STR_BEGIN_OBJECT\n'
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)'
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 = obj[${sanitizedKey}]
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, 'value')}
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
- return json + JSON_STR_END_OBJECT
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; i < mergedLocations.length; i++) {
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
- return context.functionsNamesBySchema.get(schema)
507
+ const funcName = context.functionsNamesBySchema.get(schema)
508
+ return `json += ${funcName}(${input})`
493
509
  }
494
510
 
495
- const functionName = generateFuncName(context)
496
- context.functionsNamesBySchema.set(schema, functionName)
511
+ const nullable = schema.nullable === true
497
512
 
498
- let schemaRef = location.getSchemaRef()
499
- if (schemaRef.startsWith(context.rootSchemaId)) {
500
- schemaRef = schemaRef.replace(context.rootSchemaId, '')
501
- }
513
+ const schemaId = location.schemaId || ''
514
+ const jsonPointer = location.jsonPointer || ''
515
+ const fullPath = `${schemaId}#${jsonPointer}`
502
516
 
503
- let functionCode = `
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
- const nullable = schema.nullable === true
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
- ${buildInnerObject(context, location)}
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
- context.functions.push(functionCode)
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
- return context.functionsNamesBySchema.get(schema)
566
+ const funcName = context.functionsNamesBySchema.get(schema)
567
+ return `json += ${funcName}(${input})`
535
568
  }
536
569
 
537
- const functionName = generateFuncName(context)
538
- context.functionsNamesBySchema.set(schema, functionName)
570
+ const nullable = schema.nullable === true
539
571
 
540
- let schemaRef = location.getSchemaRef()
541
- if (schemaRef.startsWith(context.rootSchemaId)) {
542
- schemaRef = schemaRef.replace(context.rootSchemaId, '')
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
- let functionCode = `
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
- const nullable = schema.nullable === true
551
- functionCode += `
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
- if (!schema.additionalItems && Array.isArray(itemsSchema)) {
560
- functionCode += `
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
- functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
683
+ inlinedCode += `if (arrayLength_${objVar} >= ${largeArraySize}) json += JSON.stringify(${objVar})\n else {`
569
684
  }
570
685
 
571
- functionCode += `
572
- const arrayEnd = arrayLength - 1
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
- for (let i = 0; i < itemsSchema.length; i++) {
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
- functionCode += `value = obj[${i}]`
581
- const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), 'value')
582
- functionCode += `
583
- if (${i} < arrayLength) {
584
- if (${buildArrayTypeCondition(item.type, 'value')}) {
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
- functionCode += `
598
- for (let i = ${itemsSchema.length}; i < arrayLength; i++) {
599
- value = obj[i]
600
- json += JSON.stringify(value)
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
- functionCode += `
609
- for (let i = 0; i < arrayLength; i++) {
610
- value = obj[i]
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
- functionCode += `
619
- return JSON_STR_BEGIN_ARRAY + json + JSON_STR_END_ARRAY
620
- }`
729
+ inlinedCode += `
730
+ json += JSON_STR_END_ARRAY
731
+ `
621
732
 
622
- context.functions.push(functionCode)
623
- return functionName
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 = 'value === null'
746
+ condition = `${accessor} === null`
631
747
  break
632
748
  case 'string':
633
- condition = `typeof value === 'string' ||
634
- value === null ||
635
- value instanceof Date ||
636
- value instanceof RegExp ||
749
+ condition = `typeof ${accessor} === 'string' ||
750
+ ${accessor} === null ||
751
+ ${accessor} instanceof Date ||
752
+ ${accessor} instanceof RegExp ||
637
753
  (
638
- typeof value === "object" &&
639
- typeof value.toString === "function" &&
640
- value.toString !== Object.prototype.toString
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 = 'Number.isInteger(value)'
760
+ condition = `Number.isInteger(${accessor})`
645
761
  break
646
762
  case 'number':
647
- condition = 'Number.isFinite(value)'
763
+ condition = `Number.isFinite(${accessor})`
648
764
  break
649
765
  case 'boolean':
650
- condition = 'typeof value === \'boolean\''
766
+ condition = `typeof ${accessor} === 'boolean'`
651
767
  break
652
768
  case 'object':
653
- condition = 'value && typeof value === \'object\' && value.constructor === Object'
769
+ condition = `${accessor} && typeof ${accessor} === 'object' && ${accessor}.constructor === Object`
654
770
  break
655
771
  case 'array':
656
- condition = 'Array.isArray(value)'
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 '${schemaRef}' does not match schema definition.\`)
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
- const funcName = buildObject(context, location)
783
- return `json += ${funcName}(${input})`
899
+ return buildObject(context, location, input)
784
900
  }
785
901
  case 'array': {
786
- const funcName = buildArray(context, location)
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; i < allOf.length; i++) {
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; index < oneOfs.length; index++) {
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 '${schemaRef}' does not match schema definition.\`)
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/serializer.js CHANGED
@@ -1,5 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ // eslint-disable-next-line
4
+ const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/
5
+
3
6
  module.exports = class Serializer {
4
7
  constructor (options) {
5
8
  switch (options && options.rounding) {
@@ -116,7 +119,7 @@ module.exports = class Serializer {
116
119
  }
117
120
  }
118
121
  return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
119
- } else if (len < 5000 && str.isWellFormed()) {
122
+ } else if (len < 5000 && STR_ESCAPE.test(str) === false) {
120
123
  // Only use the regular expression for shorter input. The overhead is otherwise too much.
121
124
  return '"' + str + '"'
122
125
  } else {
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, date) => {
25
- return date instanceof Date
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 js date format. In order to properly validate objects containing a date,
55
- // it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
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.1.0",
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,107 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+
5
+ const build = require('..')
6
+
7
+ test('serialize string with newlines - issue #793', (t) => {
8
+ t.plan(2)
9
+
10
+ const schema = {
11
+ type: 'object',
12
+ properties: {
13
+ message: {
14
+ type: 'string'
15
+ }
16
+ }
17
+ }
18
+
19
+ const input = {
20
+ message: `This is a string
21
+ with multiple
22
+ newlines in it
23
+ Foo`
24
+ }
25
+
26
+ const stringify = build(schema)
27
+ const output = stringify(input)
28
+
29
+ // The output should be valid JSON
30
+ t.assert.doesNotThrow(() => {
31
+ JSON.parse(output)
32
+ }, 'JSON output should be parseable')
33
+
34
+ // The parsed output should match the input
35
+ const parsed = JSON.parse(output)
36
+ t.assert.equal(parsed.message, input.message)
37
+ })
38
+
39
+ test('serialize string with various newline characters - issue #793', (t) => {
40
+ t.plan(4)
41
+
42
+ const schema = {
43
+ type: 'string'
44
+ }
45
+
46
+ const stringify = build(schema)
47
+
48
+ // Test \n (line feed)
49
+ const inputLF = 'line1\nline2'
50
+ const outputLF = stringify(inputLF)
51
+ t.assert.equal(JSON.parse(outputLF), inputLF)
52
+
53
+ // Test \r (carriage return)
54
+ const inputCR = 'line1\rline2'
55
+ const outputCR = stringify(inputCR)
56
+ t.assert.equal(JSON.parse(outputCR), inputCR)
57
+
58
+ // Test \r\n (CRLF)
59
+ const inputCRLF = 'line1\r\nline2'
60
+ const outputCRLF = stringify(inputCRLF)
61
+ t.assert.equal(JSON.parse(outputCRLF), inputCRLF)
62
+
63
+ // Test mixed newlines
64
+ const inputMixed = 'line1\nline2\rline3\r\nline4'
65
+ const outputMixed = stringify(inputMixed)
66
+ t.assert.equal(JSON.parse(outputMixed), inputMixed)
67
+ })
68
+
69
+ test('serialize object with newlines in multiple properties - issue #793', (t) => {
70
+ t.plan(2)
71
+
72
+ const schema = {
73
+ type: 'object',
74
+ properties: {
75
+ message: {
76
+ type: 'string'
77
+ },
78
+ description: {
79
+ type: 'string'
80
+ },
81
+ timestamp: {
82
+ type: 'string'
83
+ }
84
+ }
85
+ }
86
+
87
+ const input = {
88
+ message: `This is a string
89
+ with multiple
90
+ newlines in it
91
+ Foo`,
92
+ description: 'This JSON response contains a field with newline characters',
93
+ timestamp: new Date().toISOString()
94
+ }
95
+
96
+ const stringify = build(schema)
97
+ const output = stringify(input)
98
+
99
+ // The output should be valid JSON
100
+ t.assert.doesNotThrow(() => {
101
+ JSON.parse(output)
102
+ }, 'JSON output should be parseable')
103
+
104
+ // The parsed output should match the input
105
+ const parsed = JSON.parse(output)
106
+ t.assert.deepEqual(parsed, input)
107
+ })
@@ -0,0 +1,177 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+
5
+ const build = require('..')
6
+
7
+ test('serialize string with quotes - issue #794', (t) => {
8
+ t.plan(2)
9
+
10
+ const schema = {
11
+ type: 'object',
12
+ properties: {
13
+ message: {
14
+ type: 'string'
15
+ }
16
+ }
17
+ }
18
+
19
+ const input = {
20
+ message: 'Error: Property "name" is required'
21
+ }
22
+
23
+ const stringify = build(schema)
24
+ const output = stringify(input)
25
+
26
+ // The output should be valid JSON
27
+ t.assert.doesNotThrow(() => {
28
+ JSON.parse(output)
29
+ }, 'JSON output should be parseable')
30
+
31
+ // The parsed output should match the input
32
+ const parsed = JSON.parse(output)
33
+ t.assert.equal(parsed.message, input.message)
34
+ })
35
+
36
+ test('serialize string with various quote types - issue #794', (t) => {
37
+ t.plan(6)
38
+
39
+ const schema = {
40
+ type: 'string'
41
+ }
42
+
43
+ const stringify = build(schema)
44
+
45
+ // Test double quotes
46
+ const inputDoubleQuotes = 'Property "name" is required'
47
+ const outputDoubleQuotes = stringify(inputDoubleQuotes)
48
+ t.assert.doesNotThrow(() => JSON.parse(outputDoubleQuotes))
49
+ t.assert.equal(JSON.parse(outputDoubleQuotes), inputDoubleQuotes)
50
+
51
+ // Test single quotes (should be fine but test for completeness)
52
+ const inputSingleQuotes = "Property 'name' is required"
53
+ const outputSingleQuotes = stringify(inputSingleQuotes)
54
+ t.assert.doesNotThrow(() => JSON.parse(outputSingleQuotes))
55
+ t.assert.equal(JSON.parse(outputSingleQuotes), inputSingleQuotes)
56
+
57
+ // Test mixed quotes
58
+ const inputMixedQuotes = 'Error: "Property \'name\' is required"'
59
+ const outputMixedQuotes = stringify(inputMixedQuotes)
60
+ t.assert.doesNotThrow(() => JSON.parse(outputMixedQuotes))
61
+ t.assert.equal(JSON.parse(outputMixedQuotes), inputMixedQuotes)
62
+ })
63
+
64
+ test('serialize error-like object with quotes in message - issue #794', (t) => {
65
+ t.plan(2)
66
+
67
+ const schema = {
68
+ type: 'object',
69
+ properties: {
70
+ error: {
71
+ type: 'object',
72
+ properties: {
73
+ message: {
74
+ type: 'string'
75
+ },
76
+ code: {
77
+ type: 'string'
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ const input = {
85
+ error: {
86
+ message: 'Validation failed: Property "email" must be a valid email address',
87
+ code: 'VALIDATION_ERROR'
88
+ }
89
+ }
90
+
91
+ const stringify = build(schema)
92
+ const output = stringify(input)
93
+
94
+ // The output should be valid JSON
95
+ t.assert.doesNotThrow(() => {
96
+ JSON.parse(output)
97
+ }, 'JSON output should be parseable')
98
+
99
+ // The parsed output should match the input
100
+ const parsed = JSON.parse(output)
101
+ t.assert.deepEqual(parsed, input)
102
+ })
103
+
104
+ test('serialize validation errors array with quotes - issue #794', (t) => {
105
+ t.plan(2)
106
+
107
+ const schema = {
108
+ type: 'object',
109
+ properties: {
110
+ errors: {
111
+ type: 'array',
112
+ items: {
113
+ type: 'object',
114
+ properties: {
115
+ message: {
116
+ type: 'string'
117
+ },
118
+ field: {
119
+ type: 'string'
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ const input = {
128
+ errors: [
129
+ {
130
+ message: 'Property "name" is required',
131
+ field: 'name'
132
+ },
133
+ {
134
+ message: 'Property "email" must be a valid email address',
135
+ field: 'email'
136
+ },
137
+ {
138
+ message: 'Value must be between "1" and "100"',
139
+ field: 'age'
140
+ }
141
+ ]
142
+ }
143
+
144
+ const stringify = build(schema)
145
+ const output = stringify(input)
146
+
147
+ // The output should be valid JSON
148
+ t.assert.doesNotThrow(() => {
149
+ JSON.parse(output)
150
+ }, 'JSON output should be parseable')
151
+
152
+ // The parsed output should match the input
153
+ const parsed = JSON.parse(output)
154
+ t.assert.deepEqual(parsed, input)
155
+ })
156
+
157
+ test('serialize string with backslashes and quotes - issue #794', (t) => {
158
+ t.plan(4)
159
+
160
+ const schema = {
161
+ type: 'string'
162
+ }
163
+
164
+ const stringify = build(schema)
165
+
166
+ // Test backslashes
167
+ const inputBackslash = 'Path: C:\\Users\\test\\file.json'
168
+ const outputBackslash = stringify(inputBackslash)
169
+ t.assert.doesNotThrow(() => JSON.parse(outputBackslash))
170
+ t.assert.equal(JSON.parse(outputBackslash), inputBackslash)
171
+
172
+ // Test combination of backslashes and quotes
173
+ const inputMixed = 'Error: Could not find file "C:\\Users\\test\\config.json"'
174
+ const outputMixed = stringify(inputMixed)
175
+ t.assert.doesNotThrow(() => JSON.parse(outputMixed))
176
+ t.assert.equal(JSON.parse(outputMixed), inputMixed)
177
+ })
@@ -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
+ })