fast-json-stringify 5.3.0 → 5.4.1

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.
@@ -15,3 +15,4 @@ jobs:
15
15
  uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
16
16
  with:
17
17
  license-check: true
18
+ lint: true
package/.taprc CHANGED
@@ -1,3 +1,9 @@
1
1
  ts: false
2
2
  jsx: false
3
- coverage: false
3
+ branches: 65
4
+ functions: 100
5
+ lines: 89
6
+ statements: 89
7
+ nyc-arg:
8
+ - "--exclude=.cache/*"
9
+ - "--exclude=lib/schema-validator.js"
package/README.md CHANGED
@@ -18,31 +18,37 @@ fast-json-stringify requires a [JSON Schema Draft 7](https://json-schema.org/spe
18
18
  ##### Benchmarks
19
19
 
20
20
  - Machine: `EX41S-SSD, Intel Core i7, 4Ghz, 64GB RAM, 4C/8T, SSD`.
21
- - Node.js `v16.9.1`
21
+ - Node.js `v18.12.1`
22
22
 
23
23
  ```
24
- FJS creation x 8,443 ops/sec ±1.01% (90 runs sampled)
25
- CJS creation x 183,219 ops/sec ±0.13% (96 runs sampled)
26
- AJV Serialize creation x 83,541,848 ops/sec ±0.24% (98 runs sampled)
27
- JSON.stringify array x 5,363 ops/sec ±0.11% (100 runs sampled)
28
- fast-json-stringify array x 6,747 ops/sec ±0.13% (98 runs sampled)
29
- compile-json-stringify array x 7,121 ops/sec ±0.42% (98 runs sampled)
30
- AJV Serialize array x 7,533 ops/sec ±0.13% (98 runs sampled)
31
- JSON.stringify long string x 16,461 ops/sec ±0.12% (98 runs sampled)
32
- fast-json-stringify long string x 16,443 ops/sec ±0.37% (99 runs sampled)
33
- compile-json-stringify long string x 16,458 ops/sec ±0.09% (98 runs sampled)
34
- AJV Serialize long string x 21,433 ops/sec ±0.08% (95 runs sampled)
35
- JSON.stringify short string x 12,035,664 ops/sec ±0.62% (96 runs sampled)
36
- fast-json-stringify short string x 38,281,060 ops/sec ±0.24% (98 runs sampled)
37
- compile-json-stringify short string x 32,388,037 ops/sec ±0.27% (97 runs sampled)
38
- AJV Serialize short string x 32,288,612 ops/sec ±0.32% (95 runs sampled)
39
- JSON.stringify obj x 3,068,185 ops/sec ±0.16% (98 runs sampled)
40
- fast-json-stringify obj x 10,082,694 ops/sec ±0.10% (97 runs sampled)
41
- compile-json-stringify obj x 17,037,963 ops/sec ±1.17% (97 runs sampled)
42
- AJV Serialize obj x 9,660,041 ops/sec ±0.11% (97 runs sampled)
43
- JSON stringify date x 1,084,008 ops/sec ±0.16% (98 runs sampled)
44
- fast-json-stringify date format x 1,781,044 ops/sec ±0.48% (99 runs sampled)
45
- compile-json-stringify date format x 1,086,187 ops/sec ±0.16% (99 runs sampled)
24
+ FJS creation x 4,129 ops/sec ±0.82% (92 runs sampled)
25
+ CJS creation x 184,196 ops/sec ±0.12% (97 runs sampled)
26
+ AJV Serialize creation x 61,130,591 ops/sec ±0.40% (92 runs sampled)
27
+ JSON.stringify array x 5,057 ops/sec ±0.10% (100 runs sampled)
28
+ fast-json-stringify array default x 6,243 ops/sec ±0.14% (98 runs sampled)
29
+ fast-json-stringify array json-stringify x 6,261 ops/sec ±0.30% (99 runs sampled)
30
+ compile-json-stringify array x 6,842 ops/sec ±0.18% (96 runs sampled)
31
+ AJV Serialize array x 6,964 ops/sec ±0.11% (95 runs sampled)
32
+ JSON.stringify large array x 248 ops/sec ±0.07% (90 runs sampled)
33
+ fast-json-stringify large array default x 99.96 ops/sec ±0.22% (74 runs sampled)
34
+ fast-json-stringify large array json-stringify x 248 ops/sec ±0.07% (90 runs sampled)
35
+ compile-json-stringify large array x 317 ops/sec ±0.09% (89 runs sampled)
36
+ AJV Serialize large array x 111 ops/sec ±0.07% (33 runs sampled)
37
+ JSON.stringify long string x 16,002 ops/sec ±0.09% (98 runs sampled)
38
+ fast-json-stringify long string x 15,979 ops/sec ±0.09% (96 runs sampled)
39
+ compile-json-stringify long string x 15,952 ops/sec ±0.31% (97 runs sampled)
40
+ AJV Serialize long string x 21,416 ops/sec ±0.08% (98 runs sampled)
41
+ JSON.stringify short string x 12,944,272 ops/sec ±0.09% (96 runs sampled)
42
+ fast-json-stringify short string x 30,585,790 ops/sec ±0.27% (97 runs sampled)
43
+ compile-json-stringify short string x 30,656,406 ops/sec ±0.12% (96 runs sampled)
44
+ AJV Serialize short string x 30,406,785 ops/sec ±0.37% (96 runs sampled)
45
+ JSON.stringify obj x 3,153,043 ops/sec ±0.33% (99 runs sampled)
46
+ fast-json-stringify obj x 6,866,434 ops/sec ±0.11% (100 runs sampled)
47
+ compile-json-stringify obj x 15,886,723 ops/sec ±0.15% (98 runs sampled)
48
+ AJV Serialize obj x 8,969,043 ops/sec ±0.36% (97 runs sampled)
49
+ JSON stringify date x 1,126,547 ops/sec ±0.09% (97 runs sampled)
50
+ fast-json-stringify date format x 1,836,188 ops/sec ±0.12% (99 runs sampled)
51
+ compile-json-stringify date format x 1,125,735 ops/sec ±0.19% (98 runs sampled)
46
52
  ```
47
53
 
48
54
  #### Table of contents:
package/index.js CHANGED
@@ -10,6 +10,7 @@ const validate = require('./lib/schema-validator')
10
10
  const Serializer = require('./lib/serializer')
11
11
  const Validator = require('./lib/validator')
12
12
  const RefResolver = require('./lib/ref-resolver')
13
+ const Location = require('./lib/location')
13
14
 
14
15
  let largeArraySize = 2e4
15
16
  let largeArrayMechanism = 'default'
@@ -40,21 +41,13 @@ function isValidSchema (schema, name) {
40
41
  }
41
42
  }
42
43
 
43
- function mergeLocation (location, key) {
44
- return {
45
- schema: location.schema[key],
46
- schemaId: location.schemaId,
47
- jsonPointer: location.jsonPointer + '/' + key
48
- }
49
- }
50
-
51
44
  function resolveRef (location, ref) {
52
45
  let hashIndex = ref.indexOf('#')
53
46
  if (hashIndex === -1) {
54
47
  hashIndex = ref.length
55
48
  }
56
49
 
57
- const schemaId = ref.slice(0, hashIndex) || location.schemaId
50
+ const schemaId = ref.slice(0, hashIndex) || location.getOriginSchemaId()
58
51
  const jsonPointer = ref.slice(hashIndex) || '#'
59
52
 
60
53
  const schema = refResolver.getSchema(schemaId, jsonPointer)
@@ -63,39 +56,38 @@ function resolveRef (location, ref) {
63
56
  throw new Error(`Cannot find reference "${ref}"`)
64
57
  }
65
58
 
59
+ const newLocation = new Location(schema, schemaId, jsonPointer)
66
60
  if (schema.$ref !== undefined) {
67
- return resolveRef({ schema, schemaId, jsonPointer }, schema.$ref)
61
+ return resolveRef(newLocation, schema.$ref)
68
62
  }
69
63
 
70
- return { schema, schemaId, jsonPointer }
64
+ return newLocation
71
65
  }
72
66
 
73
67
  const contextFunctionsNamesBySchema = new Map()
74
68
 
75
69
  let rootSchemaId = null
76
70
  let refResolver = null
77
- let validator = null
78
71
  let contextFunctions = null
72
+ let validatorSchemasIds = null
79
73
 
80
74
  function build (schema, options) {
81
75
  contextFunctionsNamesBySchema.clear()
82
76
 
83
77
  contextFunctions = []
78
+ validatorSchemasIds = new Set()
84
79
  options = options || {}
85
80
 
86
81
  refResolver = new RefResolver()
87
- validator = new Validator(options.ajv)
88
82
 
89
83
  rootSchemaId = schema.$id || randomUUID()
90
84
 
91
85
  isValidSchema(schema)
92
- validator.addSchema(schema, rootSchemaId)
93
86
  refResolver.addSchema(schema, rootSchemaId)
94
87
 
95
88
  if (options.schema) {
96
89
  for (const key of Object.keys(options.schema)) {
97
90
  isValidSchema(options.schema[key], key)
98
- validator.addSchema(options.schema[key], key)
99
91
  refResolver.addSchema(options.schema[key], key)
100
92
  }
101
93
  }
@@ -110,7 +102,7 @@ function build (schema, options) {
110
102
  if (validLargeArrayMechanisms.includes(options.largeArrayMechanism)) {
111
103
  largeArrayMechanism = options.largeArrayMechanism
112
104
  } else {
113
- throw new Error(`Unsupported large array mechanism ${options.rounding}`)
105
+ throw new Error(`Unsupported large array mechanism ${options.largeArrayMechanism}`)
114
106
  }
115
107
  }
116
108
 
@@ -122,9 +114,7 @@ function build (schema, options) {
122
114
  }
123
115
  }
124
116
 
125
- const serializer = new Serializer(options)
126
-
127
- const location = { schema, schemaId: rootSchemaId, jsonPointer: '#' }
117
+ const location = new Location(schema, rootSchemaId)
128
118
  const code = buildValue(location, 'input')
129
119
 
130
120
  const contextFunctionCode = `
@@ -135,7 +125,20 @@ function build (schema, options) {
135
125
  }
136
126
  ${contextFunctions.join('\n')}
137
127
  return main
138
- `
128
+ `
129
+
130
+ const serializer = new Serializer(options)
131
+ const validator = new Validator(options.ajv)
132
+
133
+ for (const schemaId of validatorSchemasIds) {
134
+ const schema = refResolver.getSchema(schemaId)
135
+ validator.addSchema(schema, schemaId)
136
+
137
+ const dependencies = refResolver.getSchemaDependencies(schemaId)
138
+ for (const [schemaId, schema] of Object.entries(dependencies)) {
139
+ validator.addSchema(schema, schemaId)
140
+ }
141
+ }
139
142
 
140
143
  const dependenciesName = ['validator', 'serializer', contextFunctionCode]
141
144
 
@@ -154,8 +157,9 @@ function build (schema, options) {
154
157
 
155
158
  if (options.mode === 'standalone') {
156
159
  // lazy load
160
+ const isValidatorUsed = validatorSchemasIds.size > 0
157
161
  const buildStandaloneCode = require('./lib/standalone')
158
- return buildStandaloneCode(options, validator, contextFunctionCode)
162
+ return buildStandaloneCode(options, validator, isValidatorUsed, contextFunctionCode)
159
163
  }
160
164
 
161
165
  /* eslint no-new-func: "off" */
@@ -163,9 +167,9 @@ function build (schema, options) {
163
167
  const stringifyFunc = contextFunc(validator, serializer)
164
168
 
165
169
  refResolver = null
166
- validator = null
167
170
  rootSchemaId = null
168
171
  contextFunctions = null
172
+ validatorSchemasIds = null
169
173
  contextFunctionsNamesBySchema.clear()
170
174
 
171
175
  return stringifyFunc
@@ -228,106 +232,82 @@ function inferTypeByKeyword (schema) {
228
232
  return schema.type
229
233
  }
230
234
 
231
- function addPatternProperties (location) {
235
+ function buildExtraObjectPropertiesSerializer (location) {
232
236
  const schema = location.schema
233
- const pp = schema.patternProperties
237
+ const propertiesKeys = Object.keys(schema.properties || {})
238
+
234
239
  let code = `
235
- var properties = ${JSON.stringify(schema.properties)} || {}
236
- var keys = Object.keys(obj)
237
- for (var i = 0; i < keys.length; i++) {
238
- if (properties[keys[i]]) continue
240
+ const propertiesKeys = ${JSON.stringify(propertiesKeys)}
241
+ for (const [key, value] of Object.entries(obj)) {
242
+ if (
243
+ propertiesKeys.includes(key) ||
244
+ value === undefined ||
245
+ typeof value === 'function' ||
246
+ typeof value === 'symbol'
247
+ ) continue
239
248
  `
240
249
 
241
- const patternPropertiesLocation = mergeLocation(location, 'patternProperties')
242
- Object.keys(pp).forEach((regex) => {
243
- let ppLocation = mergeLocation(patternPropertiesLocation, regex)
244
- if (pp[regex].$ref) {
245
- ppLocation = resolveRef(ppLocation, pp[regex].$ref)
246
- pp[regex] = ppLocation.schema
247
- }
248
-
249
- try {
250
- RegExp(regex)
251
- } catch (err) {
252
- throw new Error(`${err.message}. Found at ${regex} matching ${JSON.stringify(pp[regex])}`)
253
- }
250
+ const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
251
+ const patternPropertiesSchema = patternPropertiesLocation.schema
254
252
 
255
- const valueCode = buildValue(ppLocation, 'obj[keys[i]]')
256
- code += `
257
- if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {
258
- ${addComma}
259
- json += serializer.asString(keys[i]) + ':'
260
- ${valueCode}
261
- continue
262
- }
263
- `
264
- })
265
- if (schema.additionalProperties) {
266
- code += additionalProperty(location)
267
- }
253
+ if (patternPropertiesSchema !== undefined) {
254
+ for (const propertyKey in patternPropertiesSchema) {
255
+ const propertyLocation = patternPropertiesLocation.getPropertyLocation(propertyKey)
268
256
 
269
- code += `
257
+ try {
258
+ RegExp(propertyKey)
259
+ } catch (err) {
260
+ const jsonPointer = propertyLocation.getSchemaRef()
261
+ throw new Error(`${err.message}. Invalid pattern property regexp key ${propertyKey} at ${jsonPointer}`)
270
262
  }
271
- `
272
- return code
273
- }
274
263
 
275
- function additionalProperty (location) {
276
- const ap = location.schema.additionalProperties
277
- let code = ''
278
- if (ap === true) {
279
- code += `
280
- if (obj[keys[i]] !== undefined && typeof obj[keys[i]] !== 'function' && typeof obj[keys[i]] !== 'symbol') {
264
+ code += `
265
+ if (/${propertyKey.replace(/\\*\//g, '\\/')}/.test(key)) {
281
266
  ${addComma}
282
- json += serializer.asString(keys[i]) + ':' + JSON.stringify(obj[keys[i]])
267
+ json += serializer.asString(key) + ':'
268
+ ${buildValue(propertyLocation, 'value')}
269
+ continue
283
270
  }
284
- `
285
-
286
- return code
271
+ `
272
+ }
287
273
  }
288
274
 
289
- let apLocation = mergeLocation(location, 'additionalProperties')
290
- if (apLocation.schema.$ref) {
291
- apLocation = resolveRef(apLocation, apLocation.schema.$ref)
292
- }
275
+ const additionalPropertiesLocation = location.getPropertyLocation('additionalProperties')
276
+ const additionalPropertiesSchema = additionalPropertiesLocation.schema
293
277
 
294
- const valueCode = buildValue(apLocation, 'obj[keys[i]]')
278
+ if (additionalPropertiesSchema !== undefined) {
279
+ if (additionalPropertiesSchema === true) {
280
+ code += `
281
+ ${addComma}
282
+ json += serializer.asString(key) + ':' + JSON.stringify(value)
283
+ `
284
+ } else {
285
+ const propertyLocation = location.getPropertyLocation('additionalProperties')
286
+ code += `
287
+ ${addComma}
288
+ json += serializer.asString(key) + ':'
289
+ ${buildValue(propertyLocation, 'value')}
290
+ `
291
+ }
292
+ }
295
293
 
296
294
  code += `
297
- ${addComma}
298
- json += serializer.asString(keys[i]) + ':'
299
- ${valueCode}
295
+ }
300
296
  `
301
-
302
297
  return code
303
298
  }
304
299
 
305
- function addAdditionalProperties (location) {
306
- return `
307
- var properties = ${JSON.stringify(location.schema.properties)} || {}
308
- var keys = Object.keys(obj)
309
- for (var i = 0; i < keys.length; i++) {
310
- if (properties[keys[i]]) continue
311
- ${additionalProperty(location)}
312
- }
313
- `
314
- }
315
-
316
- function buildCode (location) {
317
- if (location.schema.$ref) {
318
- location = resolveRef(location, location.schema.$ref)
319
- }
320
-
300
+ function buildInnerObject (location) {
321
301
  const schema = location.schema
322
302
  const required = schema.required || []
323
303
 
324
304
  let code = ''
325
305
 
326
- const propertiesLocation = mergeLocation(location, 'properties')
306
+ const propertiesLocation = location.getPropertyLocation('properties')
327
307
  Object.keys(schema.properties || {}).forEach((key) => {
328
- let propertyLocation = mergeLocation(propertiesLocation, key)
329
- if (propertyLocation.$ref) {
330
- propertyLocation = resolveRef(location, propertyLocation.$ref)
308
+ let propertyLocation = propertiesLocation.getPropertyLocation(key)
309
+ if (propertyLocation.schema.$ref) {
310
+ propertyLocation = resolveRef(location, propertyLocation.schema.$ref)
331
311
  }
332
312
 
333
313
  const sanitized = JSON.stringify(key)
@@ -368,17 +348,21 @@ function buildCode (location) {
368
348
  code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
369
349
  }
370
350
 
351
+ if (schema.patternProperties || schema.additionalProperties) {
352
+ code += buildExtraObjectPropertiesSerializer(location)
353
+ }
354
+
371
355
  return code
372
356
  }
373
357
 
374
358
  function mergeAllOfSchema (location, schema, mergedSchema) {
375
- const allOfLocation = mergeLocation(location, 'allOf')
359
+ const allOfLocation = location.getPropertyLocation('allOf')
376
360
 
377
361
  for (let i = 0; i < schema.allOf.length; i++) {
378
362
  let allOfSchema = schema.allOf[i]
379
363
 
380
364
  if (allOfSchema.$ref) {
381
- const allOfSchemaLocation = mergeLocation(allOfLocation, i)
365
+ const allOfSchemaLocation = allOfLocation.getPropertyLocation(i)
382
366
  allOfSchema = resolveRef(allOfSchemaLocation, allOfSchema.$ref).schema
383
367
  }
384
368
 
@@ -466,24 +450,13 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
466
450
  delete mergedSchema.allOf
467
451
 
468
452
  mergedSchema.$id = `merged_${randomUUID()}`
469
- validator.addSchema(mergedSchema)
470
453
  refResolver.addSchema(mergedSchema)
471
- location.schemaId = mergedSchema.$id
472
- location.jsonPointer = '#'
454
+ location.addMergedSchema(mergedSchema, mergedSchema.$id)
473
455
  }
474
456
 
475
- function buildInnerObject (location) {
476
- const schema = location.schema
477
- let code = buildCode(location)
478
- if (schema.patternProperties) {
479
- code += addPatternProperties(location)
480
- } else if (schema.additionalProperties && !schema.patternProperties) {
481
- code += addAdditionalProperties(location)
482
- }
483
- return code
484
- }
457
+ function addIfThenElse (location, input) {
458
+ validatorSchemasIds.add(location.getSchemaId())
485
459
 
486
- function addIfThenElse (location) {
487
460
  const schema = merge({}, location.schema)
488
461
  const thenSchema = schema.then
489
462
  const elseSchema = schema.else || { additionalProperties: true }
@@ -492,41 +465,22 @@ function addIfThenElse (location) {
492
465
  delete schema.then
493
466
  delete schema.else
494
467
 
495
- const ifLocation = mergeLocation(location, 'if')
496
- const ifSchemaRef = ifLocation.schemaId + ifLocation.jsonPointer
468
+ const ifLocation = location.getPropertyLocation('if')
469
+ const ifSchemaRef = ifLocation.getSchemaRef()
497
470
 
498
- let code = `
499
- if (validator.validate("${ifSchemaRef}", obj)) {
500
- `
501
-
502
- const thenLocation = mergeLocation(location, 'then')
471
+ const thenLocation = location.getPropertyLocation('then')
503
472
  thenLocation.schema = merge(schema, thenSchema)
504
473
 
505
- if (thenSchema.if && thenSchema.then) {
506
- code += addIfThenElse(thenLocation)
507
- } else {
508
- code += buildInnerObject(thenLocation)
509
- }
510
- code += `
511
- }
512
- `
513
-
514
- const elseLocation = mergeLocation(location, 'else')
474
+ const elseLocation = location.getPropertyLocation('else')
515
475
  elseLocation.schema = merge(schema, elseSchema)
516
476
 
517
- code += `
518
- else {
519
- `
520
-
521
- if (elseSchema.if && elseSchema.then) {
522
- code += addIfThenElse(elseLocation)
523
- } else {
524
- code += buildInnerObject(elseLocation)
525
- }
526
- code += `
527
- }
528
- `
529
- return code
477
+ return `
478
+ if (validator.validate("${ifSchemaRef}", ${input})) {
479
+ ${buildValue(thenLocation, input)}
480
+ } else {
481
+ ${buildValue(elseLocation, input)}
482
+ }
483
+ `
530
484
  }
531
485
 
532
486
  function toJSON (variableName) {
@@ -546,10 +500,14 @@ function buildObject (location) {
546
500
  const functionName = generateFuncName()
547
501
  contextFunctionsNamesBySchema.set(schema, functionName)
548
502
 
549
- const schemaId = location.schemaId === rootSchemaId ? '' : location.schemaId
503
+ let schemaRef = location.getSchemaRef()
504
+ if (schemaRef.startsWith(rootSchemaId)) {
505
+ schemaRef = schemaRef.replace(rootSchemaId, '')
506
+ }
507
+
550
508
  let functionCode = `
551
509
  function ${functionName} (input) {
552
- // ${schemaId + location.jsonPointer}
510
+ // ${schemaRef}
553
511
  `
554
512
 
555
513
  functionCode += `
@@ -558,12 +516,7 @@ function buildObject (location) {
558
516
  var addComma = false
559
517
  `
560
518
 
561
- if (schema.if && schema.then) {
562
- functionCode += addIfThenElse(location)
563
- } else {
564
- functionCode += buildInnerObject(location)
565
- }
566
-
519
+ functionCode += buildInnerObject(location)
567
520
  functionCode += `
568
521
  json += '}'
569
522
  return json
@@ -577,7 +530,7 @@ function buildObject (location) {
577
530
  function buildArray (location) {
578
531
  const schema = location.schema
579
532
 
580
- let itemsLocation = mergeLocation(location, 'items')
533
+ let itemsLocation = location.getPropertyLocation('items')
581
534
  itemsLocation.schema = itemsLocation.schema || {}
582
535
 
583
536
  if (itemsLocation.schema.$ref) {
@@ -593,10 +546,14 @@ function buildArray (location) {
593
546
  const functionName = generateFuncName()
594
547
  contextFunctionsNamesBySchema.set(schema, functionName)
595
548
 
596
- const schemaId = location.schemaId === rootSchemaId ? '' : location.schemaId
549
+ let schemaRef = location.getSchemaRef()
550
+ if (schemaRef.startsWith(rootSchemaId)) {
551
+ schemaRef = schemaRef.replace(rootSchemaId, '')
552
+ }
553
+
597
554
  let functionCode = `
598
555
  function ${functionName} (obj) {
599
- // ${schemaId + location.jsonPointer}
556
+ // ${schemaRef}
600
557
  `
601
558
 
602
559
  functionCode += `
@@ -629,7 +586,7 @@ function buildArray (location) {
629
586
  if (Array.isArray(itemsSchema)) {
630
587
  for (let i = 0; i < itemsSchema.length; i++) {
631
588
  const item = itemsSchema[i]
632
- const tmpRes = buildValue(mergeLocation(itemsLocation, i), `obj[${i}]`)
589
+ const tmpRes = buildValue(itemsLocation.getPropertyLocation(i), `obj[${i}]`)
633
590
  functionCode += `
634
591
  if (${i} < arrayLength) {
635
592
  if (${buildArrayTypeCondition(item.type, `[${i}]`)}) {
@@ -725,11 +682,11 @@ function buildMultiTypeSerializer (location, input) {
725
682
 
726
683
  let code = ''
727
684
 
728
- const locationClone = clone(location)
729
685
  types.forEach((type, index) => {
686
+ location.schema = { ...location.schema, type }
687
+ const nestedResult = buildSingleTypeSerializer(location, input)
688
+
730
689
  const statement = index === 0 ? 'if' : 'else if'
731
- locationClone.schema.type = type
732
- const nestedResult = buildSingleTypeSerializer(locationClone, input)
733
690
  switch (type) {
734
691
  case 'null':
735
692
  code += `
@@ -869,11 +826,13 @@ function buildValue (location, input) {
869
826
  }
870
827
  }
871
828
 
829
+ if (schema.if && schema.then) {
830
+ return addIfThenElse(location, input)
831
+ }
832
+
872
833
  if (schema.allOf) {
873
- const mergedSchema = clone(schema)
874
- mergeAllOfSchema(location, schema, mergedSchema)
875
- schema = mergedSchema
876
- location.schema = mergedSchema
834
+ mergeAllOfSchema(location, schema, clone(schema))
835
+ schema = location.schema
877
836
  }
878
837
 
879
838
  const type = schema.type
@@ -881,12 +840,14 @@ function buildValue (location, input) {
881
840
  let code = ''
882
841
 
883
842
  if (type === undefined && (schema.anyOf || schema.oneOf)) {
843
+ validatorSchemasIds.add(location.getSchemaId())
844
+
884
845
  const type = schema.anyOf ? 'anyOf' : 'oneOf'
885
- const anyOfLocation = mergeLocation(location, type)
846
+ const anyOfLocation = location.getPropertyLocation(type)
886
847
 
887
848
  for (let index = 0; index < location.schema[type].length; index++) {
888
- const optionLocation = mergeLocation(anyOfLocation, index)
889
- const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer
849
+ const optionLocation = anyOfLocation.getPropertyLocation(index)
850
+ const schemaRef = optionLocation.getSchemaRef()
890
851
  const nestedResult = buildValue(optionLocation, input)
891
852
  code += `
892
853
  ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+
3
+ class Location {
4
+ constructor (schema, schemaId, jsonPointer = '#') {
5
+ this.schema = schema
6
+ this.schemaId = schemaId
7
+ this.jsonPointer = jsonPointer
8
+ this.mergedSchemaId = null
9
+ }
10
+
11
+ getPropertyLocation (propertyName) {
12
+ const propertyLocation = new Location(
13
+ this.schema[propertyName],
14
+ this.schemaId,
15
+ this.jsonPointer + '/' + propertyName
16
+ )
17
+
18
+ if (this.mergedSchemaId !== null) {
19
+ propertyLocation.addMergedSchema(
20
+ this.schema[propertyName],
21
+ this.mergedSchemaId,
22
+ this.jsonPointer + '/' + propertyName
23
+ )
24
+ }
25
+
26
+ return propertyLocation
27
+ }
28
+
29
+ // Use this method to get current schema location.
30
+ // Use it when you need to create reference to the current location.
31
+ getSchemaId () {
32
+ return this.mergedSchemaId || this.schemaId
33
+ }
34
+
35
+ // Use this method to get original schema id for resolving user schema $refs
36
+ // Don't join it with a JSON pointer to get the current location.
37
+ getOriginSchemaId () {
38
+ return this.schemaId
39
+ }
40
+
41
+ getSchemaRef () {
42
+ const schemaId = this.getSchemaId()
43
+ return schemaId + this.jsonPointer
44
+ }
45
+
46
+ addMergedSchema (mergedSchema, schemaId, jsonPointer = '#') {
47
+ this.schema = mergedSchema
48
+ this.mergedSchemaId = schemaId
49
+ this.jsonPointer = jsonPointer
50
+ }
51
+ }
52
+
53
+ module.exports = Location
@@ -28,6 +28,18 @@ class RefResolver {
28
28
  return getDataByJSONPointer(schema.schema, jsonPointer)
29
29
  }
30
30
 
31
+ getSchemaDependencies (schemaId, dependencies = {}) {
32
+ const schema = this.schemas[schemaId]
33
+
34
+ for (const dependencySchemaId of schema.dependencies) {
35
+ if (dependencies[dependencySchemaId] !== undefined) continue
36
+ dependencies[dependencySchemaId] = this.getSchema(dependencySchemaId)
37
+ this.getSchemaDependencies(dependencySchemaId, dependencies)
38
+ }
39
+
40
+ return dependencies
41
+ }
42
+
31
43
  insertSchemaBySchemaId (schema, schemaId) {
32
44
  if (
33
45
  this.schemas[schemaId] !== undefined &&
@@ -35,7 +47,7 @@ class RefResolver {
35
47
  ) {
36
48
  throw new Error(`There is already another schema with id ${schemaId}`)
37
49
  }
38
- this.schemas[schemaId] = { schema, anchors: {} }
50
+ this.schemas[schemaId] = { schema, anchors: {}, dependencies: [] }
39
51
  }
40
52
 
41
53
  insertSchemaByAnchor (schema, schemaId, anchor) {
@@ -60,6 +72,14 @@ class RefResolver {
60
72
  }
61
73
  }
62
74
 
75
+ const ref = schema.$ref
76
+ if (ref !== undefined && typeof ref === 'string') {
77
+ if (ref.charAt(0) !== '#') {
78
+ const dependencySchemaId = ref.split('#')[0]
79
+ this.schemas[rootSchemaId].dependencies.push(dependencySchemaId)
80
+ }
81
+ }
82
+
63
83
  for (const key in schema) {
64
84
  if (typeof schema[key] === 'object' && schema[key] !== null) {
65
85
  this.insertSchemaSubschemas(schema[key], rootSchemaId)
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]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/
5
+
3
6
  module.exports = class Serializer {
4
7
  constructor (options = {}) {
5
8
  switch (options.rounding) {
@@ -94,6 +97,11 @@ module.exports = class Serializer {
94
97
  str = str.toString()
95
98
  }
96
99
 
100
+ // Fast escape chars check
101
+ if (!STR_ESCAPE.test(str)) {
102
+ return quotes + str + quotes
103
+ }
104
+
97
105
  if (str.length < 42) {
98
106
  return this.asStringSmall(str)
99
107
  } else {
package/lib/standalone.js CHANGED
@@ -1,20 +1,21 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
3
 
4
- function buildStandaloneCode (options, validator, contextFunctionCode) {
4
+ function buildStandaloneCode (options, validator, isValidatorUsed, contextFunctionCode) {
5
5
  const serializerCode = fs.readFileSync(path.join(__dirname, 'serializer.js')).toString()
6
6
  let buildAjvCode = ''
7
- let defaultAjvSchema = ''
8
- const defaultMeta = validator.ajv.defaultMeta()
9
- if (typeof defaultMeta === 'string') {
10
- defaultAjvSchema = defaultMeta
11
- } else {
12
- defaultAjvSchema = defaultMeta.$id || defaultMeta.id
13
- }
14
- const shouldUseAjv = contextFunctionCode.indexOf('validator') !== -1
15
- // we need to export the custom json schema
16
7
  let ajvSchemasCode = ''
17
- if (shouldUseAjv) {
8
+
9
+ if (isValidatorUsed) {
10
+ let defaultAjvSchema = ''
11
+ // we need to export the custom json schema
12
+ const defaultMeta = validator.ajv.defaultMeta()
13
+ if (typeof defaultMeta === 'string') {
14
+ defaultAjvSchema = defaultMeta
15
+ } else {
16
+ defaultAjvSchema = defaultMeta.$id || defaultMeta.id
17
+ }
18
+
18
19
  ajvSchemasCode += `const validator = new Validator(${JSON.stringify(options.ajv || {})})\n`
19
20
  for (const [id, schema] of Object.entries(validator.ajv.schemas)) {
20
21
  // should skip ajv default schema
package/package.json CHANGED
@@ -1,21 +1,13 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.3.0",
3
+ "version": "5.4.1",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
- "scripts": {
8
- "bench": "node ./benchmark/bench.js",
9
- "bench:cmp": "node ./benchmark/bench-cmp-branch.js",
10
- "bench:cmp:ci": "node ./benchmark/bench-cmp-branch.js --ci",
11
- "benchmark": "node ./benchmark/bench-cmp-lib.js",
12
- "lint:fix": "standard --fix",
13
- "test:lint": "standard",
14
- "test:typescript": "tsc --project ./test/types/tsconfig.json && tsd",
15
- "test:unit": "tap -J test/*.test.js test/**/*.test.js",
16
- "test": "npm run test:lint && npm run test:unit && npm run test:typescript"
17
- },
18
- "precommit": "test",
7
+ "precommit": [
8
+ "lint",
9
+ "test"
10
+ ],
19
11
  "repository": {
20
12
  "type": "git",
21
13
  "url": "git+https://github.com/fastify/fast-json-stringify.git"
@@ -34,7 +26,7 @@
34
26
  "homepage": "https://github.com/fastify/fast-json-stringify#readme",
35
27
  "devDependencies": {
36
28
  "@fastify/pre-commit": "^2.0.2",
37
- "@sinclair/typebox": "^0.24.9",
29
+ "@sinclair/typebox": "^0.25.2",
38
30
  "benchmark": "^2.1.4",
39
31
  "cli-select": "^1.1.2",
40
32
  "compile-json-stringify": "^0.1.2",
@@ -45,7 +37,7 @@
45
37
  "simple-git": "^3.7.1",
46
38
  "standard": "^17.0.0",
47
39
  "tap": "^16.0.1",
48
- "tsd": "^0.22.0",
40
+ "tsd": "^0.24.1",
49
41
  "typescript": "^4.0.2",
50
42
  "webpack": "^5.40.0"
51
43
  },
@@ -65,5 +57,16 @@
65
57
  "runkitExampleFilename": "./examples/example.js",
66
58
  "tsd": {
67
59
  "directory": "test/types"
60
+ },
61
+ "scripts": {
62
+ "bench": "node ./benchmark/bench.js",
63
+ "bench:cmp": "node ./benchmark/bench-cmp-branch.js",
64
+ "bench:cmp:ci": "node ./benchmark/bench-cmp-branch.js --ci",
65
+ "benchmark": "node ./benchmark/bench-cmp-lib.js",
66
+ "lint": "standard",
67
+ "lint:fix": "standard --fix",
68
+ "test:typescript": "tsc --project ./test/types/tsconfig.json && tsd",
69
+ "test:unit": "tap -J test/*.test.js test/**/*.test.js",
70
+ "test": "npm run test:unit && npm run test:typescript"
68
71
  }
69
- }
72
+ }
@@ -437,3 +437,48 @@ test('object with external $refs in allOf', (t) => {
437
437
  })
438
438
  t.equal(value, '{"id1":1,"id2":2}')
439
439
  })
440
+
441
+ test('allof with local anchor reference', (t) => {
442
+ t.plan(1)
443
+
444
+ const externalSchemas = {
445
+ Test: {
446
+ $id: 'Test',
447
+ definitions: {
448
+ Problem: {
449
+ type: 'object',
450
+ properties: {
451
+ type: {
452
+ type: 'string'
453
+ }
454
+ }
455
+ },
456
+ ValidationFragment: {
457
+ type: 'string'
458
+ },
459
+ ValidationErrorProblem: {
460
+ type: 'object',
461
+ allOf: [
462
+ {
463
+ $ref: '#/definitions/Problem'
464
+ },
465
+ {
466
+ type: 'object',
467
+ properties: {
468
+ validation: {
469
+ $ref: '#/definitions/ValidationFragment'
470
+ }
471
+ }
472
+ }
473
+ ]
474
+ }
475
+ }
476
+ }
477
+ }
478
+
479
+ const schema = { $ref: 'Test#/definitions/ValidationErrorProblem' }
480
+ const stringify = build(schema, { schema: externalSchemas })
481
+ const data = { type: 'foo', validation: 'bar' }
482
+
483
+ t.equal(stringify(data), JSON.stringify(data))
484
+ })
@@ -596,3 +596,51 @@ test('anyOf object with invalid field date of type string with format or null',
596
596
  const withOneOfStringify = build(withOneOfSchema)
597
597
  t.throws(() => withOneOfStringify({ prop: toStringify }))
598
598
  })
599
+
600
+ test('anyOf with a nested external schema', (t) => {
601
+ t.plan(1)
602
+
603
+ const externalSchemas = {
604
+ schema1: {
605
+ definitions: {
606
+ def1: {
607
+ $id: 'external',
608
+ type: 'string'
609
+ }
610
+ },
611
+ type: 'number'
612
+ }
613
+ }
614
+ const schema = { anyOf: [{ $ref: 'external' }] }
615
+
616
+ const stringify = build(schema, { schema: externalSchemas })
617
+ t.equal(stringify('foo'), '"foo"')
618
+ })
619
+
620
+ test('object with ref and validated properties', (t) => {
621
+ t.plan(1)
622
+
623
+ const externalSchemas = {
624
+ RefSchema: {
625
+ $id: 'RefSchema',
626
+ type: 'string'
627
+ }
628
+ }
629
+
630
+ const schema = {
631
+ $id: 'root',
632
+ type: 'object',
633
+ properties: {
634
+ id: {
635
+ anyOf: [
636
+ { type: 'string' },
637
+ { type: 'number' }
638
+ ]
639
+ },
640
+ reference: { $ref: 'RefSchema' }
641
+ }
642
+ }
643
+
644
+ const stringify = build(schema, { schema: externalSchemas })
645
+ t.equal(stringify({ id: 1, reference: 'hi' }), '{"id":1,"reference":"hi"}')
646
+ })
@@ -4,6 +4,24 @@ const test = require('tap').test
4
4
  const validator = require('is-my-json-valid')
5
5
  const build = require('..')
6
6
 
7
+ test('error on invalid largeArrayMechanism', (t) => {
8
+ t.plan(1)
9
+
10
+ t.throws(() => build({
11
+ title: 'large array of null values with default mechanism',
12
+ type: 'object',
13
+ properties: {
14
+ ids: {
15
+ type: 'array',
16
+ items: { type: 'null' }
17
+ }
18
+ }
19
+ }, {
20
+ largeArraySize: 2e4,
21
+ largeArrayMechanism: 'invalid'
22
+ }), Error('Unsupported large array mechanism invalid'))
23
+ })
24
+
7
25
  function buildTest (schema, toStringify, options) {
8
26
  test(`render a ${schema.title} as JSON`, (t) => {
9
27
  t.plan(3)
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const t = require('tap')
4
+ const { DateTime } = require('luxon')
4
5
  const build = require('..')
5
6
 
6
7
  const schema = {
@@ -368,3 +369,52 @@ t.test('nested if/then', t => {
368
369
  JSON.stringify({ a: 'A', bar: 'bar', bar1: 'bar1' })
369
370
  )
370
371
  })
372
+
373
+ t.test('if/else with string format', (t) => {
374
+ t.plan(2)
375
+
376
+ const schema = {
377
+ if: { type: 'string' },
378
+ then: { type: 'string', format: 'date' },
379
+ else: { const: 'Invalid' }
380
+ }
381
+
382
+ const stringify = build(schema)
383
+
384
+ const date = new Date()
385
+
386
+ t.equal(stringify(date), `"${DateTime.fromJSDate(date).toISODate()}"`)
387
+ t.equal(stringify('Invalid'), '"Invalid"')
388
+ })
389
+
390
+ t.test('if/else with const integers', (t) => {
391
+ t.plan(2)
392
+
393
+ const schema = {
394
+ type: 'number',
395
+ if: { type: 'number', minimum: 42 },
396
+ then: { const: 66 },
397
+ else: { const: 33 }
398
+ }
399
+
400
+ const stringify = build(schema)
401
+
402
+ t.equal(stringify(100.32), '66')
403
+ t.equal(stringify(10.12), '33')
404
+ })
405
+
406
+ t.test('if/else with array', (t) => {
407
+ t.plan(2)
408
+
409
+ const schema = {
410
+ type: 'array',
411
+ if: { type: 'array', maxItems: 1 },
412
+ then: { items: { type: 'string' } },
413
+ else: { items: { type: 'number' } }
414
+ }
415
+
416
+ const stringify = build(schema)
417
+
418
+ t.equal(stringify(['1']), JSON.stringify(['1']))
419
+ t.equal(stringify(['1', '2']), JSON.stringify([1, 2]))
420
+ })
package/test/ref.test.js CHANGED
@@ -1981,3 +1981,27 @@ test('nested schema should overwrite anchor scope', (t) => {
1981
1981
  t.equal(output, JSON.stringify(data))
1982
1982
  t.throws(() => build({ $ref: 'root#anchor' }, { schema: externalSchema }))
1983
1983
  })
1984
+
1985
+ test('object property reference with default value', (t) => {
1986
+ t.plan(1)
1987
+
1988
+ const schema = {
1989
+ definitions: {
1990
+ prop: {
1991
+ type: 'string',
1992
+ default: 'foo'
1993
+ }
1994
+ },
1995
+ type: 'object',
1996
+ properties: {
1997
+ prop: {
1998
+ $ref: '#/definitions/prop'
1999
+ }
2000
+ }
2001
+ }
2002
+
2003
+ const stringify = build(schema)
2004
+ const output = stringify({})
2005
+
2006
+ t.equal(output, '{"prop":"foo"}')
2007
+ })