fast-json-stringify 3.1.0 → 4.1.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/index.js CHANGED
@@ -2,15 +2,14 @@
2
2
 
3
3
  /* eslint no-prototype-builtins: 0 */
4
4
 
5
- const Ajv = require('ajv')
6
- const fastUri = require('fast-uri')
7
- const ajvFormats = require('ajv-formats')
8
5
  const merge = require('deepmerge')
9
6
  const clone = require('rfdc')({ proto: true })
10
7
  const fjsCloned = Symbol('fast-json-stringify.cloned')
11
8
  const { randomUUID } = require('crypto')
12
9
 
13
10
  const validate = require('./schema-validator')
11
+ const Serializer = require('./serializer')
12
+ const buildAjv = require('./ajv')
14
13
 
15
14
  let largeArraySize = 2e4
16
15
  let stringSimilarity = null
@@ -52,16 +51,20 @@ function mergeLocation (source, dest) {
52
51
 
53
52
  const arrayItemsReferenceSerializersMap = new Map()
54
53
  const objectReferenceSerializersMap = new Map()
54
+ const schemaReferenceMap = new Map()
55
+
55
56
  let ajvInstance = null
57
+ let contextFunctions = null
56
58
 
57
59
  function build (schema, options) {
58
60
  arrayItemsReferenceSerializersMap.clear()
59
61
  objectReferenceSerializersMap.clear()
62
+ schemaReferenceMap.clear()
60
63
 
64
+ contextFunctions = []
61
65
  options = options || {}
62
66
 
63
- ajvInstance = new Ajv({ ...options.ajv, strictSchema: false, uriResolver: fastUri })
64
- ajvFormats(ajvInstance)
67
+ ajvInstance = buildAjv(options.ajv)
65
68
 
66
69
  isValidSchema(schema)
67
70
  if (options.schema) {
@@ -71,11 +74,8 @@ function build (schema, options) {
71
74
  }
72
75
  }
73
76
 
74
- let intParseFunctionName = 'trunc'
75
77
  if (options.rounding) {
76
- if (['floor', 'ceil', 'round'].includes(options.rounding)) {
77
- intParseFunctionName = options.rounding
78
- } else {
78
+ if (!['floor', 'ceil', 'round'].includes(options.rounding)) {
79
79
  throw new Error(`Unsupported integer rounding method ${options.rounding}`)
80
80
  }
81
81
  }
@@ -96,15 +96,7 @@ function build (schema, options) {
96
96
  }
97
97
  }
98
98
 
99
- /* eslint no-new-func: "off" */
100
- let code = `
101
- 'use strict'
102
- `
103
-
104
- code += `
105
- ${asFunctions}
106
- function parseInteger(int) { return Math.${intParseFunctionName}(int) }
107
- `
99
+ const serializer = new Serializer(options)
108
100
 
109
101
  let location = {
110
102
  schema,
@@ -121,61 +113,55 @@ function build (schema, options) {
121
113
  schema.type = inferTypeByKeyword(schema)
122
114
  }
123
115
 
124
- let main
116
+ const code = buildValue('main', 'input', location)
125
117
 
126
- switch (schema.type) {
127
- case 'object':
128
- main = '$main'
129
- code = buildObject(location, code, main)
130
- break
131
- case 'string':
132
- main = schema.nullable ? '$asStringNullable' : getStringSerializer(schema.format)
133
- break
134
- case 'integer':
135
- main = schema.nullable ? '$asIntegerNullable' : '$asInteger'
136
- break
137
- case 'number':
138
- main = schema.nullable ? '$asNumberNullable' : '$asNumber'
139
- break
140
- case 'boolean':
141
- main = schema.nullable ? '$asBooleanNullable' : '$asBoolean'
142
- break
143
- case 'null':
144
- main = '$asNull'
145
- break
146
- case 'array':
147
- main = '$main'
148
- code = buildArray(location, code, main)
149
- schema = location.schema
150
- break
151
- case undefined:
152
- main = '$asAny'
153
- break
154
- default:
155
- throw new Error(`${schema.type} unsupported`)
118
+ const contextFunctionCode = `
119
+ function main (input) {
120
+ let json = ''
121
+ ${code}
122
+ return json
123
+ }
124
+ ${contextFunctions.join('\n')}
125
+ return main
126
+ `
127
+
128
+ const dependenciesName = ['ajv', 'serializer', contextFunctionCode]
129
+
130
+ if (options.debugMode) {
131
+ options.mode = 'debug'
156
132
  }
157
133
 
158
- code += `
159
- ;
160
- return ${main}
161
- `
162
- const dependencies = [ajvInstance]
163
- const dependenciesName = ['ajv']
164
- ajvInstance = null
134
+ if (options.mode === 'debug') {
135
+ return { code: dependenciesName.join('\n'), ajv: ajvInstance }
136
+ }
165
137
 
166
- dependenciesName.push(code)
138
+ if (options.mode === 'standalone') {
139
+ return `
140
+ 'use strict'
167
141
 
168
- if (options.debugMode) {
169
- return {
170
- code: dependenciesName.join('\n'),
171
- ajv: dependencies[0]
172
- }
142
+ const Serializer = require('fast-json-stringify/serializer')
143
+ const buildAjv = require('fast-json-stringify/ajv')
144
+
145
+ const serializer = new Serializer(${JSON.stringify(options || {})})
146
+ const ajv = buildAjv(${JSON.stringify(options.ajv || {})})
147
+
148
+ ${contextFunctionCode.replace('return main', '')}
149
+
150
+ module.exports = main
151
+ `
173
152
  }
174
153
 
154
+ /* eslint no-new-func: "off" */
155
+ const contextFunc = new Function('ajv', 'serializer', contextFunctionCode)
156
+ const stringifyFunc = contextFunc(ajvInstance, serializer)
157
+
158
+ ajvInstance = null
159
+ contextFunctions = null
175
160
  arrayItemsReferenceSerializersMap.clear()
176
161
  objectReferenceSerializersMap.clear()
162
+ schemaReferenceMap.clear()
177
163
 
178
- return (Function.apply(null, dependenciesName).apply(null, dependencies))
164
+ return stringifyFunc
179
165
  }
180
166
 
181
167
  const objectKeywords = [
@@ -235,171 +221,15 @@ function inferTypeByKeyword (schema) {
235
221
  return schema.type
236
222
  }
237
223
 
238
- const stringSerializerMap = {
239
- 'date-time': '$asDatetime',
240
- date: '$asDate',
241
- time: '$asTime'
242
- }
243
-
244
- function getStringSerializer (format) {
245
- return stringSerializerMap[format] ||
246
- '$asString'
247
- }
248
-
249
- function getTestSerializer (format) {
250
- return stringSerializerMap[format]
251
- }
252
-
253
- const asFunctions = `
254
- function $pad2Zeros (num) {
255
- const s = '00' + num
256
- return s[s.length - 2] + s[s.length - 1]
257
- }
258
-
259
- function $asAny (i) {
260
- return JSON.stringify(i)
261
- }
262
-
263
- function $asNull () {
264
- return 'null'
265
- }
266
-
267
- function $asInteger (i) {
268
- if (typeof i === 'bigint') {
269
- return i.toString()
270
- } else if (Number.isInteger(i)) {
271
- return $asNumber(i)
272
- } else {
273
- /* eslint no-undef: "off" */
274
- return $asNumber(parseInteger(i))
275
- }
276
- }
277
-
278
- function $asIntegerNullable (i) {
279
- return i === null ? null : $asInteger(i)
280
- }
281
-
282
- function $asNumber (i) {
283
- const num = Number(i)
284
- if (isNaN(num)) {
285
- return 'null'
286
- } else {
287
- return '' + num
288
- }
289
- }
290
-
291
- function $asNumberNullable (i) {
292
- return i === null ? null : $asNumber(i)
293
- }
294
-
295
- function $asBoolean (bool) {
296
- return bool && 'true' || 'false' // eslint-disable-line
297
- }
298
-
299
- function $asBooleanNullable (bool) {
300
- return bool === null ? null : $asBoolean(bool)
301
- }
302
-
303
- function $asDatetime (date, skipQuotes) {
304
- const quotes = skipQuotes === true ? '' : '"'
305
- if (date instanceof Date) {
306
- return quotes + date.toISOString() + quotes
307
- } else if (date && typeof date.toISOString === 'function') {
308
- return quotes + date.toISOString() + quotes
309
- } else {
310
- return $asString(date, skipQuotes)
311
- }
312
- }
313
-
314
- function $asDate (date, skipQuotes) {
315
- const quotes = skipQuotes === true ? '' : '"'
316
- if (date instanceof Date) {
317
- return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000 )).toISOString().slice(0, 10) + quotes
318
- } else if (date && typeof date.format === 'function') {
319
- return quotes + date.format('YYYY-MM-DD') + quotes
320
- } else {
321
- return $asString(date, skipQuotes)
224
+ function getStringSerializer (format, nullable) {
225
+ switch (format) {
226
+ case 'date-time': return nullable ? 'serializer.asDatetimeNullable.bind(serializer)' : 'serializer.asDatetime.bind(serializer)'
227
+ case 'date': return nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
228
+ case 'time': return nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
229
+ default: return nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
322
230
  }
323
231
  }
324
232
 
325
- function $asTime (date, skipQuotes) {
326
- const quotes = skipQuotes === true ? '' : '"'
327
- if (date instanceof Date) {
328
- const hour = new Intl.DateTimeFormat('en', { hour: 'numeric', hour12: false }).format(date)
329
- const minute = new Intl.DateTimeFormat('en', { minute: 'numeric' }).format(date)
330
- const second = new Intl.DateTimeFormat('en', { second: 'numeric' }).format(date)
331
- return quotes + $pad2Zeros(hour) + ':' + $pad2Zeros(minute) + ':' + $pad2Zeros(second) + quotes
332
- } else if (date && typeof date.format === 'function') {
333
- return quotes + date.format('HH:mm:ss') + quotes
334
- } else {
335
- return $asString(date, skipQuotes)
336
- }
337
- }
338
-
339
- function $asString (str, skipQuotes) {
340
- const quotes = skipQuotes === true ? '' : '"'
341
- if (str instanceof Date) {
342
- return quotes + str.toISOString() + quotes
343
- } else if (str === null) {
344
- return quotes + quotes
345
- } else if (str instanceof RegExp) {
346
- str = str.source
347
- } else if (typeof str !== 'string') {
348
- str = str.toString()
349
- }
350
- // If we skipQuotes it means that we are using it as test
351
- // no need to test the string length for the render
352
- if (skipQuotes) {
353
- return str
354
- }
355
-
356
- if (str.length < 42) {
357
- return $asStringSmall(str)
358
- } else {
359
- return JSON.stringify(str)
360
- }
361
- }
362
-
363
- function $asStringNullable (str) {
364
- return str === null ? null : $asString(str)
365
- }
366
-
367
- // magically escape strings for json
368
- // relying on their charCodeAt
369
- // everything below 32 needs JSON.stringify()
370
- // every string that contain surrogate needs JSON.stringify()
371
- // 34 and 92 happens all the time, so we
372
- // have a fast case for them
373
- function $asStringSmall (str) {
374
- const l = str.length
375
- let result = ''
376
- let last = 0
377
- let found = false
378
- let surrogateFound = false
379
- let point = 255
380
- // eslint-disable-next-line
381
- for (var i = 0; i < l && point >= 32; i++) {
382
- point = str.charCodeAt(i)
383
- if (point >= 0xD800 && point <= 0xDFFF) {
384
- // The current character is a surrogate.
385
- surrogateFound = true
386
- }
387
- if (point === 34 || point === 92) {
388
- result += str.slice(last, i) + '\\\\'
389
- last = i
390
- found = true
391
- }
392
- }
393
-
394
- if (!found) {
395
- result = str
396
- } else {
397
- result += str.slice(last)
398
- }
399
- return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
400
- }
401
- `
402
-
403
233
  function addPatternProperties (location) {
404
234
  const schema = location.schema
405
235
  const pp = schema.patternProperties
@@ -409,81 +239,28 @@ function addPatternProperties (location) {
409
239
  for (var i = 0; i < keys.length; i++) {
410
240
  if (properties[keys[i]]) continue
411
241
  `
242
+
412
243
  Object.keys(pp).forEach((regex, index) => {
413
244
  let ppLocation = mergeLocation(location, { schema: pp[regex] })
414
245
  if (pp[regex].$ref) {
415
246
  ppLocation = refFinder(pp[regex].$ref, location)
416
247
  pp[regex] = ppLocation.schema
417
248
  }
418
- const type = pp[regex].type
419
- const format = pp[regex].format
420
- const stringSerializer = getStringSerializer(format)
249
+
421
250
  try {
422
251
  RegExp(regex)
423
252
  } catch (err) {
424
253
  throw new Error(`${err.message}. Found at ${regex} matching ${JSON.stringify(pp[regex])}`)
425
254
  }
426
255
 
427
- const ifPpKeyExists = `if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {`
428
-
429
- if (type === 'object') {
430
- code += `${buildObject(ppLocation, '', 'buildObjectPP' + index)}
431
- ${ifPpKeyExists}
432
- ${addComma}
433
- json += $asString(keys[i]) + ':' + buildObjectPP${index}(obj[keys[i]])
434
- `
435
- } else if (type === 'array') {
436
- code += `${buildArray(ppLocation, '', 'buildArrayPP' + index)}
437
- ${ifPpKeyExists}
438
- ${addComma}
439
- json += $asString(keys[i]) + ':' + buildArrayPP${index}(obj[keys[i]])
440
- `
441
- } else if (type === 'null') {
442
- code += `
443
- ${ifPpKeyExists}
444
- ${addComma}
445
- json += $asString(keys[i]) +':null'
446
- `
447
- } else if (type === 'string') {
448
- code += `
449
- ${ifPpKeyExists}
450
- ${addComma}
451
- json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
452
- `
453
- } else if (type === 'integer') {
454
- code += `
455
- ${ifPpKeyExists}
456
- ${addComma}
457
- json += $asString(keys[i]) + ':' + $asInteger(obj[keys[i]])
458
- `
459
- } else if (type === 'number') {
460
- code += `
461
- ${ifPpKeyExists}
462
- ${addComma}
463
- json += $asString(keys[i]) + ':' + $asNumber(obj[keys[i]])
464
- `
465
- } else if (type === 'boolean') {
466
- code += `
467
- ${ifPpKeyExists}
468
- ${addComma}
469
- json += $asString(keys[i]) + ':' + $asBoolean(obj[keys[i]])
470
- `
471
- } else if (type === undefined) {
472
- code += `
473
- ${ifPpKeyExists}
474
- ${addComma}
475
- json += $asString(keys[i]) + ':' + $asAny(obj[keys[i]])
476
- `
477
- } else {
478
- code += `
479
- ${ifPpKeyExists}
480
- throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ' + ${JSON.stringify(type)})
481
- `
482
- }
483
-
256
+ const valueCode = buildValue('', 'obj[keys[i]]', ppLocation)
484
257
  code += `
485
- continue
486
- }
258
+ if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {
259
+ ${addComma}
260
+ json += serializer.asString(keys[i]) + ':'
261
+ ${valueCode}
262
+ continue
263
+ }
487
264
  `
488
265
  })
489
266
  if (schema.additionalProperties) {
@@ -500,12 +277,14 @@ function additionalProperty (location) {
500
277
  let ap = location.schema.additionalProperties
501
278
  let code = ''
502
279
  if (ap === true) {
503
- return `
280
+ code += `
504
281
  if (obj[keys[i]] !== undefined && typeof obj[keys[i]] !== 'function' && typeof obj[keys[i]] !== 'symbol') {
505
282
  ${addComma}
506
- json += $asString(keys[i]) + ':' + JSON.stringify(obj[keys[i]])
283
+ json += serializer.asString(keys[i]) + ':' + JSON.stringify(obj[keys[i]])
507
284
  }
508
285
  `
286
+
287
+ return code
509
288
  }
510
289
  let apLocation = mergeLocation(location, { schema: ap })
511
290
  if (ap.$ref) {
@@ -513,60 +292,14 @@ function additionalProperty (location) {
513
292
  ap = apLocation.schema
514
293
  }
515
294
 
516
- const type = ap.type
517
- const format = ap.format
518
- const stringSerializer = getStringSerializer(format)
519
- if (type === 'object') {
520
- code += `${buildObject(apLocation, '', 'buildObjectAP')}
521
- ${addComma}
522
- json += $asString(keys[i]) + ':' + buildObjectAP(obj[keys[i]])
523
- `
524
- } else if (type === 'array') {
525
- code += `${buildArray(apLocation, '', 'buildArrayAP')}
526
- ${addComma}
527
- json += $asString(keys[i]) + ':' + buildArrayAP(obj[keys[i]])
528
- `
529
- } else if (type === 'null') {
530
- code += `
531
- ${addComma}
532
- json += $asString(keys[i]) +':null'
533
- `
534
- } else if (type === 'string') {
535
- code += `
536
- ${addComma}
537
- json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
538
- `
539
- } else if (type === 'integer') {
540
- code += `
541
- var t = Number(obj[keys[i]])
542
- if (!isNaN(t)) {
543
- ${addComma}
544
- json += $asString(keys[i]) + ':' + t
545
- }
546
- `
547
- } else if (type === 'number') {
548
- code += `
549
- var t = Number(obj[keys[i]])
550
- if (!isNaN(t)) {
551
- ${addComma}
552
- json += $asString(keys[i]) + ':' + t
553
- }
554
- `
555
- } else if (type === 'boolean') {
556
- code += `
557
- ${addComma}
558
- json += $asString(keys[i]) + ':' + $asBoolean(obj[keys[i]])
559
- `
560
- } else if (type === undefined) {
561
- code += `
562
- ${addComma}
563
- json += $asString(keys[i]) + ':' + $asAny(obj[keys[i]])
564
- `
565
- } else {
566
- code += `
567
- throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ' + ${JSON.stringify(type)})
568
- `
569
- }
295
+ const valueCode = buildValue('', 'obj[keys[i]]', apLocation)
296
+
297
+ code += `
298
+ ${addComma}
299
+ json += serializer.asString(keys[i]) + ':'
300
+ ${valueCode}
301
+ `
302
+
570
303
  return code
571
304
  }
572
305
 
@@ -605,15 +338,25 @@ function refFinder (ref, location) {
605
338
  return {
606
339
  schema: externalSchema[ref],
607
340
  root: externalSchema[ref],
608
- externalSchema: externalSchema
341
+ externalSchema
609
342
  }
610
343
  }
611
344
 
612
345
  // Split file from walk
613
346
  ref = ref.split('#')
614
347
 
615
- // If external file
616
- if (ref[0]) {
348
+ // Check schemaReferenceMap for $id entry
349
+ if (ref[0] && schemaReferenceMap.has(ref[0])) {
350
+ schema = schemaReferenceMap.get(ref[0])
351
+ root = schemaReferenceMap.get(ref[0])
352
+ if (schema.$ref) {
353
+ return refFinder(schema.$ref, {
354
+ schema,
355
+ root,
356
+ externalSchema
357
+ })
358
+ }
359
+ } else if (ref[0]) { // If external file
617
360
  schema = externalSchema[ref[0]]
618
361
  root = externalSchema[ref[0]]
619
362
 
@@ -623,9 +366,9 @@ function refFinder (ref, location) {
623
366
 
624
367
  if (schema.$ref) {
625
368
  return refFinder(schema.$ref, {
626
- schema: schema,
627
- root: root,
628
- externalSchema: externalSchema
369
+ schema,
370
+ root,
371
+ externalSchema
629
372
  })
630
373
  }
631
374
  }
@@ -652,8 +395,8 @@ function refFinder (ref, location) {
652
395
 
653
396
  return {
654
397
  schema: dereferenced,
655
- root: root,
656
- externalSchema: externalSchema
398
+ root,
399
+ externalSchema
657
400
  }
658
401
  } else {
659
402
  // eslint-disable-next-line
@@ -674,16 +417,16 @@ function refFinder (ref, location) {
674
417
 
675
418
  if (result.$ref) {
676
419
  return refFinder(result.$ref, {
677
- schema: schema,
678
- root: root,
679
- externalSchema: externalSchema
420
+ schema,
421
+ root,
422
+ externalSchema
680
423
  })
681
424
  }
682
425
 
683
426
  return {
684
427
  schema: result,
685
- root: root,
686
- externalSchema: externalSchema
428
+ root,
429
+ externalSchema
687
430
  }
688
431
 
689
432
  function findBadKey (obj, keys) {
@@ -702,15 +445,17 @@ function refFinder (ref, location) {
702
445
  }
703
446
  }
704
447
 
705
- function buildCode (location, code, laterCode, name) {
448
+ function buildCode (location, locationPath) {
706
449
  if (location.schema.$ref) {
707
450
  location = refFinder(location.schema.$ref, location)
708
451
  }
709
452
 
710
453
  const schema = location.schema
711
- let required = schema.required
454
+ const required = schema.required || []
712
455
 
713
- Object.keys(schema.properties || {}).forEach((key, i, a) => {
456
+ let code = ''
457
+
458
+ Object.keys(schema.properties || {}).forEach((key) => {
714
459
  let propertyLocation = mergeLocation(location, { schema: schema.properties[key] })
715
460
  if (schema.properties[key].$ref) {
716
461
  propertyLocation = refFinder(schema.properties[key].$ref, location)
@@ -720,61 +465,25 @@ function buildCode (location, code, laterCode, name) {
720
465
  // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
721
466
  // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
722
467
 
723
- const type = schema.properties[key].type
724
- const nullable = schema.properties[key].nullable
725
468
  const sanitized = JSON.stringify(key)
726
469
  const asString = JSON.stringify(sanitized)
727
470
 
728
- if (nullable) {
729
- code += `
730
- if (obj[${sanitized}] === null) {
731
- ${addComma}
732
- json += ${asString} + ':null'
733
- var rendered = true
734
- } else {
735
- `
736
- }
737
-
738
- if (type === 'number') {
739
- code += `
740
- var t = Number(obj[${sanitized}])
741
- if (!isNaN(t)) {
742
- ${addComma}
743
- json += ${asString} + ':' + t
744
- `
745
- } else if (type === 'integer') {
746
- code += `
747
- var rendered = false
748
- var t = $asInteger(obj[${sanitized}])
749
- if (!isNaN(t)) {
750
- ${addComma}
751
- json += ${asString} + ':' + t
752
- rendered = true
753
- }
754
- if (rendered) {
471
+ code += `
472
+ if (obj[${sanitized}] !== undefined) {
473
+ ${addComma}
474
+ json += ${asString} + ':'
755
475
  `
756
- } else {
757
- code += `
758
- if (obj[${sanitized}] !== undefined) {
759
- ${addComma}
760
- json += ${asString} + ':'
761
- `
762
476
 
763
- const result = nested(laterCode, name, key, mergeLocation(propertyLocation, { schema: schema.properties[key] }), undefined, false)
764
- code += result.code
765
- laterCode = result.laterCode
766
- }
477
+ code += buildValue(locationPath + key, `obj[${JSON.stringify(key)}]`, mergeLocation(propertyLocation, { schema: schema.properties[key] }))
767
478
 
768
479
  const defaultValue = schema.properties[key].default
769
480
  if (defaultValue !== undefined) {
770
- required = filterRequired(required, key)
771
481
  code += `
772
482
  } else {
773
483
  ${addComma}
774
484
  json += ${asString} + ':' + ${JSON.stringify(JSON.stringify(defaultValue))}
775
485
  `
776
- } else if (required && required.indexOf(key) !== -1) {
777
- required = filterRequired(required, key)
486
+ } else if (required.includes(key)) {
778
487
  code += `
779
488
  } else {
780
489
  throw new Error('${sanitized} is required!')
@@ -784,79 +493,119 @@ function buildCode (location, code, laterCode, name) {
784
493
  code += `
785
494
  }
786
495
  `
496
+ })
787
497
 
788
- if (nullable) {
789
- code += `
790
- }
791
- `
498
+ for (const requiredProperty of required) {
499
+ if (schema.properties && schema.properties[requiredProperty] !== undefined) continue
500
+ code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
501
+ }
502
+
503
+ return code
504
+ }
505
+
506
+ function mergeAllOfSchema (location, schema, mergedSchema) {
507
+ for (let allOfSchema of schema.allOf) {
508
+ if (allOfSchema.$ref) {
509
+ allOfSchema = refFinder(allOfSchema.$ref, mergeLocation(location, { schema: allOfSchema })).schema
792
510
  }
793
- })
794
511
 
795
- if (required && required.length > 0) {
796
- code += 'var required = ['
797
- // eslint-disable-next-line
798
- for (var i = 0; i < required.length; i++) {
799
- if (i > 0) {
800
- code += ','
512
+ let allOfSchemaType = allOfSchema.type
513
+ if (allOfSchemaType === undefined) {
514
+ allOfSchemaType = inferTypeByKeyword(allOfSchema)
515
+ }
516
+
517
+ if (allOfSchemaType !== undefined) {
518
+ if (
519
+ mergedSchema.type !== undefined &&
520
+ mergedSchema.type !== allOfSchemaType
521
+ ) {
522
+ throw new Error('allOf schemas have different type values')
801
523
  }
802
- code += `${JSON.stringify(required[i])}`
524
+ mergedSchema.type = allOfSchemaType
803
525
  }
804
- code += `]
805
- for (var i = 0; i < required.length; i++) {
806
- if (obj[required[i]] === undefined) throw new Error('"' + required[i] + '" is required!')
526
+
527
+ if (allOfSchema.format !== undefined) {
528
+ if (
529
+ mergedSchema.format !== undefined &&
530
+ mergedSchema.format !== allOfSchema.format
531
+ ) {
532
+ throw new Error('allOf schemas have different format values')
807
533
  }
808
- `
809
- }
534
+ mergedSchema.format = allOfSchema.format
535
+ }
810
536
 
811
- if (schema.allOf) {
812
- const builtCode = buildCodeWithAllOfs(location, code, laterCode, name)
813
- code = builtCode.code
814
- laterCode = builtCode.laterCode
815
- }
537
+ if (allOfSchema.nullable !== undefined) {
538
+ if (
539
+ mergedSchema.nullable !== undefined &&
540
+ mergedSchema.nullable !== allOfSchema.nullable
541
+ ) {
542
+ throw new Error('allOf schemas have different nullable values')
543
+ }
544
+ mergedSchema.nullable = allOfSchema.nullable
545
+ }
816
546
 
817
- return { code: code, laterCode: laterCode }
818
- }
547
+ if (allOfSchema.properties !== undefined) {
548
+ if (mergedSchema.properties === undefined) {
549
+ mergedSchema.properties = {}
550
+ }
551
+ Object.assign(mergedSchema.properties, allOfSchema.properties)
552
+ }
819
553
 
820
- function filterRequired (required, key) {
821
- if (!required) {
822
- return required
823
- }
824
- return required.filter(k => k !== key)
825
- }
554
+ if (allOfSchema.additionalProperties !== undefined) {
555
+ if (mergedSchema.additionalProperties === undefined) {
556
+ mergedSchema.additionalProperties = {}
557
+ }
558
+ Object.assign(mergedSchema.additionalProperties, allOfSchema.additionalProperties)
559
+ }
826
560
 
827
- function buildCodeWithAllOfs (location, code, laterCode, name) {
828
- if (location.schema.allOf) {
829
- location.schema.allOf.forEach((ss) => {
830
- const builtCode = buildCodeWithAllOfs(mergeLocation(location, { schema: ss }), code, laterCode, name)
831
- code = builtCode.code
832
- laterCode = builtCode.laterCode
833
- })
834
- } else {
835
- const builtCode = buildCode(location, code, laterCode, name)
561
+ if (allOfSchema.patternProperties !== undefined) {
562
+ if (mergedSchema.patternProperties === undefined) {
563
+ mergedSchema.patternProperties = {}
564
+ }
565
+ Object.assign(mergedSchema.patternProperties, allOfSchema.patternProperties)
566
+ }
836
567
 
837
- code = builtCode.code
838
- laterCode = builtCode.laterCode
839
- }
568
+ if (allOfSchema.required !== undefined) {
569
+ if (mergedSchema.required === undefined) {
570
+ mergedSchema.required = []
571
+ }
572
+ mergedSchema.required.push(...allOfSchema.required)
573
+ }
840
574
 
841
- return { code: code, laterCode: laterCode }
575
+ if (allOfSchema.oneOf !== undefined) {
576
+ if (mergedSchema.oneOf === undefined) {
577
+ mergedSchema.oneOf = []
578
+ }
579
+ mergedSchema.oneOf.push(...allOfSchema.oneOf)
580
+ }
581
+
582
+ if (allOfSchema.anyOf !== undefined) {
583
+ if (mergedSchema.anyOf === undefined) {
584
+ mergedSchema.anyOf = []
585
+ }
586
+ mergedSchema.anyOf.push(...allOfSchema.anyOf)
587
+ }
588
+
589
+ if (allOfSchema.allOf !== undefined) {
590
+ mergeAllOfSchema(location, allOfSchema, mergedSchema)
591
+ }
592
+ }
593
+ delete mergedSchema.allOf
842
594
  }
843
595
 
844
- function buildInnerObject (location, name) {
596
+ function buildInnerObject (location, locationPath) {
845
597
  const schema = location.schema
846
- const result = buildCodeWithAllOfs(location, '', '', name)
598
+ let code = buildCode(location, locationPath)
847
599
  if (schema.patternProperties) {
848
- result.code += addPatternProperties(location)
600
+ code += addPatternProperties(location)
849
601
  } else if (schema.additionalProperties && !schema.patternProperties) {
850
- result.code += addAdditionalProperties(location)
602
+ code += addAdditionalProperties(location)
851
603
  }
852
- return result
604
+ return code
853
605
  }
854
606
 
855
- function addIfThenElse (location, name) {
607
+ function addIfThenElse (location, locationPath) {
856
608
  let code = ''
857
- let r
858
- let laterCode = ''
859
- let innerR
860
609
 
861
610
  const schema = location.schema
862
611
  const copy = merge({}, schema)
@@ -877,14 +626,10 @@ function addIfThenElse (location, name) {
877
626
  if (valid) {
878
627
  `
879
628
  if (merged.if && merged.then) {
880
- innerR = addIfThenElse(mergedLocation, name + 'Then')
881
- code += innerR.code
882
- laterCode = innerR.laterCode
629
+ code += addIfThenElse(mergedLocation, locationPath + 'Then')
883
630
  }
884
631
 
885
- r = buildInnerObject(mergedLocation, name + 'Then')
886
- code += r.code
887
- laterCode += r.laterCode
632
+ code += buildInnerObject(mergedLocation, locationPath + 'Then')
888
633
 
889
634
  code += `
890
635
  }
@@ -897,19 +642,15 @@ function addIfThenElse (location, name) {
897
642
  `
898
643
 
899
644
  if (merged.if && merged.then) {
900
- innerR = addIfThenElse(mergedLocation, name + 'Else')
901
- code += innerR.code
902
- laterCode += innerR.laterCode
645
+ code += addIfThenElse(mergedLocation, locationPath + 'Else')
903
646
  }
904
647
 
905
- r = buildInnerObject(mergedLocation, name + 'Else')
906
- code += r.code
907
- laterCode += r.laterCode
648
+ code += buildInnerObject(mergedLocation, locationPath + 'Else')
908
649
 
909
650
  code += `
910
651
  }
911
652
  `
912
- return { code: code, laterCode: laterCode }
653
+ return code
913
654
  }
914
655
 
915
656
  function toJSON (variableName) {
@@ -919,69 +660,63 @@ function toJSON (variableName) {
919
660
  `
920
661
  }
921
662
 
922
- function buildObject (location, code, name) {
663
+ function buildObject (location, locationPath) {
923
664
  const schema = location.schema
665
+ if (schema.$id !== undefined) {
666
+ schemaReferenceMap.set(schema.$id, schema)
667
+ }
924
668
 
925
- code += `
926
- function ${name} (input) {
669
+ if (objectReferenceSerializersMap.has(schema)) {
670
+ return objectReferenceSerializersMap.get(schema)
671
+ }
672
+
673
+ const functionName = generateFuncName()
674
+ objectReferenceSerializersMap.set(schema, functionName)
675
+
676
+ let functionCode = `
677
+ function ${functionName} (input) {
678
+ // ${locationPath}
927
679
  `
928
680
  if (schema.nullable) {
929
- code += `
930
- if(input === null) {
681
+ functionCode += `
682
+ if (input === null) {
931
683
  return 'null';
932
684
  }
933
685
  `
934
686
  }
935
687
 
936
- if (objectReferenceSerializersMap.has(schema) && objectReferenceSerializersMap.get(schema) !== name) {
937
- code += `
938
- return ${objectReferenceSerializersMap.get(schema)}(input)
939
- }
940
- `
941
- return code
942
- }
943
- objectReferenceSerializersMap.set(schema, name)
944
-
945
- code += `
688
+ functionCode += `
946
689
  var obj = ${toJSON('input')}
947
690
  var json = '{'
948
691
  var addComma = false
949
692
  `
950
693
 
951
- let r
694
+ let rCode
952
695
  if (schema.if && schema.then) {
953
- code += `
696
+ functionCode += `
954
697
  var valid
955
698
  `
956
- r = addIfThenElse(location, name)
699
+ rCode = addIfThenElse(location, locationPath)
957
700
  } else {
958
- r = buildInnerObject(location, name)
701
+ rCode = buildInnerObject(location, locationPath)
959
702
  }
960
703
 
961
704
  // Removes the comma if is the last element of the string (in case there are not properties)
962
- code += `${r.code}
705
+ functionCode += `${rCode}
963
706
  json += '}'
964
707
  return json
965
708
  }
966
- ${r.laterCode}
967
709
  `
968
710
 
969
- return code
711
+ contextFunctions.push(functionCode)
712
+ return functionName
970
713
  }
971
714
 
972
- function buildArray (location, code, name, key = null) {
715
+ function buildArray (location, locationPath) {
973
716
  let schema = location.schema
974
- code += `
975
- function ${name} (obj) {
976
- `
977
- if (schema.nullable) {
978
- code += `
979
- if(obj === null) {
980
- return 'null';
981
- }
982
- `
717
+ if (schema.$id !== undefined) {
718
+ schemaReferenceMap.set(schema.$id, schema)
983
719
  }
984
- const laterCode = ''
985
720
 
986
721
  // default to any items type
987
722
  if (!schema.items) {
@@ -997,100 +732,105 @@ function buildArray (location, code, name, key = null) {
997
732
 
998
733
  location = refFinder(schema.items.$ref, location)
999
734
  schema.items = location.schema
735
+ }
1000
736
 
1001
- if (arrayItemsReferenceSerializersMap.has(schema.items)) {
1002
- code += `
1003
- return ${arrayItemsReferenceSerializersMap.get(schema.items)}(obj)
1004
- }
1005
- `
1006
- return code
1007
- }
1008
- arrayItemsReferenceSerializersMap.set(schema.items, name)
737
+ if (arrayItemsReferenceSerializersMap.has(schema.items)) {
738
+ return arrayItemsReferenceSerializersMap.get(schema.items)
1009
739
  }
1010
740
 
1011
- let result = { code: '', laterCode: '' }
1012
- const accessor = '[i]'
1013
- if (Array.isArray(schema.items)) {
1014
- result = schema.items.reduce((res, item, i) => {
1015
- const tmpRes = nested(laterCode, name, accessor, mergeLocation(location, { schema: item }), i, true)
1016
- const condition = `i === ${i} && ${buildArrayTypeCondition(item.type, accessor)}`
1017
- return {
1018
- code: `${res.code}
1019
- ${i > 0 ? 'else' : ''} if (${condition}) {
1020
- ${tmpRes.code}
1021
- }`,
1022
- laterCode: `${res.laterCode}
1023
- ${tmpRes.laterCode}`
1024
- }
1025
- }, result)
741
+ const functionName = generateFuncName()
742
+ arrayItemsReferenceSerializersMap.set(schema.items, functionName)
1026
743
 
1027
- if (schema.additionalItems) {
1028
- const tmpRes = nested(laterCode, name, accessor, mergeLocation(location, { schema: schema.items }), undefined, true)
1029
- result.code += `
1030
- else if (i >= ${schema.items.length}) {
1031
- ${tmpRes.code}
1032
- }
1033
- `
1034
- }
744
+ let functionCode = `
745
+ function ${functionName} (obj) {
746
+ // ${locationPath}
747
+ `
1035
748
 
1036
- result.code += `
1037
- else {
1038
- throw new Error(\`Item at $\{i} does not match schema definition.\`)
1039
- }
749
+ if (schema.nullable) {
750
+ functionCode += `
751
+ if (obj === null) {
752
+ return 'null';
753
+ }
1040
754
  `
1041
- } else {
1042
- result = nested(laterCode, name, accessor, mergeLocation(location, { schema: schema.items }), undefined, true)
1043
755
  }
1044
756
 
1045
- if (key) {
1046
- code += `
1047
- if(!Array.isArray(obj)) {
1048
- throw new TypeError(\`Property '${key}' should be of type array, received '$\{obj}' instead.\`)
757
+ functionCode += `
758
+ if (!Array.isArray(obj)) {
759
+ throw new TypeError(\`The value '$\{obj}' does not match schema definition.\`)
1049
760
  }
761
+ const arrayLength = obj.length
762
+ `
763
+
764
+ if (!schema.additionalItems) {
765
+ functionCode += `
766
+ if (arrayLength > ${schema.items.length}) {
767
+ throw new Error(\`Item at ${schema.items.length} does not match schema definition.\`)
768
+ }
1050
769
  `
1051
770
  }
1052
771
 
1053
- code += `
1054
- var l = obj.length
1055
- if (l && l >= ${largeArraySize}) {`
1056
-
1057
- const concatSnippet = `
772
+ if (largeArrayMechanism !== 'default') {
773
+ if (largeArrayMechanism === 'json-stringify') {
774
+ functionCode += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
775
+ } else {
776
+ throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
1058
777
  }
778
+ }
1059
779
 
1060
- var jsonOutput= ''
1061
- for (var i = 0; i < l; i++) {
1062
- var json = ''
1063
- ${result.code}
1064
- jsonOutput += json
780
+ functionCode += `
781
+ let jsonOutput = ''
782
+ `
1065
783
 
1066
- if (json.length > 0 && i < l - 1) {
1067
- jsonOutput += ','
1068
- }
784
+ const accessor = '[i]'
785
+ if (Array.isArray(schema.items)) {
786
+ for (let i = 0; i < schema.items.length; i++) {
787
+ const item = schema.items[i]
788
+ const tmpRes = buildValue(locationPath + accessor + i, `obj[${i}]`, mergeLocation(location, { schema: item }))
789
+ functionCode += `
790
+ if (${i} < arrayLength) {
791
+ if (${buildArrayTypeCondition(item.type, `[${i}]`)}) {
792
+ let json = ''
793
+ ${tmpRes}
794
+ jsonOutput += json
795
+ if (${i} < arrayLength - 1) {
796
+ jsonOutput += ','
797
+ }
798
+ } else {
799
+ throw new Error(\`Item at ${i} does not match schema definition.\`)
800
+ }
801
+ }
802
+ `
1069
803
  }
1070
- return \`[\${jsonOutput}]\`
1071
- }`
1072
-
1073
- switch (largeArrayMechanism) {
1074
- case 'default':
1075
- code += `
1076
- return \`[\${obj.map(${result.mapFnName}).join(',')}]\``
1077
- break
1078
-
1079
- case 'json-stringify':
1080
- code += `
1081
- return JSON.stringify(obj)`
1082
- break
1083
804
 
1084
- default:
1085
- throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
805
+ if (schema.additionalItems) {
806
+ functionCode += `
807
+ for (let i = ${schema.items.length}; i < arrayLength; i++) {
808
+ let json = JSON.stringify(obj[i])
809
+ jsonOutput += json
810
+ if (i < arrayLength - 1) {
811
+ jsonOutput += ','
812
+ }
813
+ }`
814
+ }
815
+ } else {
816
+ const code = buildValue(locationPath + accessor, 'obj[i]', mergeLocation(location, { schema: schema.items }))
817
+ functionCode += `
818
+ for (let i = 0; i < arrayLength; i++) {
819
+ let json = ''
820
+ ${code}
821
+ jsonOutput += json
822
+ if (i < arrayLength - 1) {
823
+ jsonOutput += ','
824
+ }
825
+ }`
1086
826
  }
1087
827
 
1088
- code += `
1089
- ${concatSnippet}
1090
- ${result.laterCode}
1091
- `
828
+ functionCode += `
829
+ return \`[\${jsonOutput}]\`
830
+ }`
1092
831
 
1093
- return code
832
+ contextFunctions.push(functionCode)
833
+ return functionName
1094
834
  }
1095
835
 
1096
836
  function buildArrayTypeCondition (type, accessor) {
@@ -1154,26 +894,12 @@ function dereferenceOfRefs (location, type) {
1154
894
  return locations
1155
895
  }
1156
896
 
1157
- let strNameCounter = 0
1158
- function asFuncName (str) {
1159
- // only allow chars that can work
1160
- let rep = str.replace(/[^a-zA-Z0-9$_]/g, '')
1161
-
1162
- if (rep.length === 0) {
1163
- return 'anan' + strNameCounter++
1164
- } else if (rep !== str) {
1165
- rep += strNameCounter++
1166
- }
1167
-
1168
- return rep
897
+ let genFuncNameCounter = 0
898
+ function generateFuncName () {
899
+ return 'anonymous' + genFuncNameCounter++
1169
900
  }
1170
901
 
1171
- function nested (laterCode, name, key, location, subKey, isArray) {
1172
- let code = ''
1173
- let funcName
1174
-
1175
- subKey = subKey || ''
1176
-
902
+ function buildValue (locationPath, input, location) {
1177
903
  let schema = location.schema
1178
904
 
1179
905
  if (schema.$ref) {
@@ -1187,121 +913,96 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1187
913
  }
1188
914
  }
1189
915
 
916
+ if (schema.allOf) {
917
+ const mergedSchema = clone(schema)
918
+ mergeAllOfSchema(location, schema, mergedSchema)
919
+ schema = mergedSchema
920
+ location.schema = mergedSchema
921
+ }
922
+
1190
923
  const type = schema.type
1191
924
  const nullable = schema.nullable === true
1192
925
 
1193
- const accessor = isArray ? key : `[${JSON.stringify(key)}]`
926
+ let code = ''
927
+ let funcName
1194
928
 
1195
929
  switch (type) {
1196
930
  case 'null':
1197
- funcName = '$asNull'
1198
931
  code += `
1199
- json += $asNull()
932
+ json += serializer.asNull()
1200
933
  `
1201
934
  break
1202
935
  case 'string': {
1203
- funcName = '$asString'
1204
- const stringSerializer = getStringSerializer(schema.format)
1205
- code += nullable ? `json += obj${accessor} === null ? null : ${stringSerializer}(obj${accessor})` : `json += ${stringSerializer}(obj${accessor})`
936
+ funcName = getStringSerializer(schema.format, nullable)
937
+ code += `json += ${funcName}(${input})`
1206
938
  break
1207
939
  }
1208
940
  case 'integer':
1209
- funcName = '$asInteger'
1210
- code += nullable ? `json += obj${accessor} === null ? null : $asInteger(obj${accessor})` : `json += $asInteger(obj${accessor})`
941
+ funcName = nullable ? 'serializer.asIntegerNullable.bind(serializer)' : 'serializer.asInteger.bind(serializer)'
942
+ code += `json += ${funcName}(${input})`
1211
943
  break
1212
944
  case 'number':
1213
- funcName = '$asNumber'
1214
- code += nullable ? `json += obj${accessor} === null ? null : $asNumber(obj${accessor})` : `json += $asNumber(obj${accessor})`
945
+ funcName = nullable ? 'serializer.asNumberNullable.bind(serializer)' : 'serializer.asNumber.bind(serializer)'
946
+ code += `json += ${funcName}(${input})`
1215
947
  break
1216
948
  case 'boolean':
1217
- funcName = '$asBoolean'
1218
- code += nullable ? `json += obj${accessor} === null ? null : $asBoolean(obj${accessor})` : `json += $asBoolean(obj${accessor})`
949
+ funcName = nullable ? 'serializer.asBooleanNullable.bind(serializer)' : 'serializer.asBoolean.bind(serializer)'
950
+ code += `json += ${funcName}(${input})`
1219
951
  break
1220
952
  case 'object':
1221
- funcName = asFuncName(name + key + subKey)
1222
- laterCode = buildObject(location, laterCode, funcName)
1223
- code += `
1224
- json += ${funcName}(obj${accessor})
1225
- `
953
+ funcName = buildObject(location, locationPath)
954
+ code += `json += ${funcName}(${input})`
1226
955
  break
1227
956
  case 'array':
1228
- funcName = asFuncName('$arr' + name + key + subKey) // eslint-disable-line
1229
- laterCode = buildArray(location, laterCode, funcName, key)
1230
- code += `
1231
- json += ${funcName}(obj${accessor})
1232
- `
957
+ funcName = buildArray(location, locationPath)
958
+ code += `json += ${funcName}(${input})`
1233
959
  break
1234
960
  case undefined:
1235
- funcName = '$asNull'
1236
- if ('anyOf' in schema) {
961
+ if (schema.anyOf || schema.oneOf) {
1237
962
  // beware: dereferenceOfRefs has side effects and changes schema.anyOf
1238
- const anyOfLocations = dereferenceOfRefs(location, 'anyOf')
1239
- anyOfLocations.forEach((location, index) => {
1240
- const nestedResult = nested(laterCode, name, key, location, subKey !== '' ? subKey : 'i' + index, isArray)
1241
- // We need a test serializer as the String serializer will not work with
1242
- // date/time ajv validations
1243
- // see: https://github.com/fastify/fast-json-stringify/issues/325
1244
- const testSerializer = getTestSerializer(location.schema.format)
1245
- const testValue = testSerializer !== undefined ? `${testSerializer}(obj${accessor}, true)` : `obj${accessor}`
1246
-
963
+ const locations = dereferenceOfRefs(location, schema.anyOf ? 'anyOf' : 'oneOf')
964
+ locations.forEach((location, index) => {
965
+ const nestedResult = buildValue(locationPath + 'i' + index, input, location)
1247
966
  // Since we are only passing the relevant schema to ajv.validate, it needs to be full dereferenced
1248
967
  // otherwise any $ref pointing to an external schema would result in an error.
1249
968
  // Full dereference of the schema happens as side effect of two functions:
1250
969
  // 1. `dereferenceOfRefs` loops through the `schema.anyOf`` array and replaces any top level reference
1251
970
  // with the actual schema
1252
- // 2. `nested`, through `buildCode`, replaces any reference in object properties with the actual schema
971
+ // 2. `buildValue`, through `buildCode`, replaces any reference in object properties with the actual schema
1253
972
  // (see https://github.com/fastify/fast-json-stringify/blob/6da3b3e8ac24b1ca5578223adedb4083b7adf8db/index.js#L631)
1254
973
 
1255
- const schemaKey = location.schema.$id || randomUUID()
1256
- ajvInstance.addSchema(location.schema, schemaKey)
1257
-
1258
- code += `
1259
- ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
1260
- ${nestedResult.code}
1261
- `
1262
- laterCode = nestedResult.laterCode
1263
- })
1264
- code += `
1265
- else json+= null
1266
- `
1267
- } else if ('oneOf' in schema) {
1268
- // beware: dereferenceOfRefs has side effects and changes schema.oneOf
1269
- const oneOfLocations = dereferenceOfRefs(location, 'oneOf')
1270
- oneOfLocations.forEach((location, index) => {
1271
- const nestedResult = nested(laterCode, name, key, location, subKey !== '' ? subKey : 'i' + index, isArray)
1272
- const testSerializer = getTestSerializer(location.schema.format)
1273
- const testValue = testSerializer !== undefined ? `${testSerializer}(obj${accessor}, true)` : `obj${accessor}`
1274
- // see comment on anyOf about dereferencing the schema before calling ajv.validate
974
+ // Ajv does not support js date format. In order to properly validate objects containing a date,
975
+ // it needs to replace all occurrences of the string date format with a custom keyword fjs_date_type.
976
+ // (see https://github.com/fastify/fast-json-stringify/pull/441)
977
+ const extendedSchema = clone(location.schema)
978
+ extendDateTimeType(extendedSchema)
1275
979
 
1276
980
  const schemaKey = location.schema.$id || randomUUID()
1277
- ajvInstance.addSchema(location.schema, schemaKey)
981
+ ajvInstance.addSchema(extendedSchema, schemaKey)
1278
982
 
1279
983
  code += `
1280
- ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
1281
- ${nestedResult.code}
984
+ ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${input}))
985
+ ${nestedResult}
1282
986
  `
1283
- laterCode = nestedResult.laterCode
1284
987
  })
1285
988
 
1286
- if (!isArray) {
1287
- code += `
1288
- else json+= null
1289
- `
1290
- }
989
+ code += `
990
+ else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
991
+ `
1291
992
  } else if (isEmpty(schema)) {
1292
993
  code += `
1293
- json += JSON.stringify(obj${accessor})
994
+ json += JSON.stringify(${input})
1294
995
  `
1295
996
  } else if ('const' in schema) {
1296
997
  code += `
1297
- if(ajv.validate(${JSON.stringify(schema)}, obj${accessor}))
998
+ if(ajv.validate(${JSON.stringify(schema)}, ${input}))
1298
999
  json += '${JSON.stringify(schema.const)}'
1299
1000
  else
1300
- throw new Error(\`Item $\{JSON.stringify(obj${accessor})} does not match schema definition.\`)
1001
+ throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`)
1301
1002
  `
1302
1003
  } else if (schema.type === undefined) {
1303
1004
  code += `
1304
- json += JSON.stringify(obj${accessor})
1005
+ json += JSON.stringify(${input})
1305
1006
  `
1306
1007
  } else {
1307
1008
  throw new Error(`${schema.type} unsupported`)
@@ -1309,70 +1010,79 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1309
1010
  break
1310
1011
  default:
1311
1012
  if (Array.isArray(type)) {
1312
- const nullIndex = type.indexOf('null')
1313
- const sortedTypes = nullIndex !== -1 ? [type[nullIndex]].concat(type.slice(0, nullIndex)).concat(type.slice(nullIndex + 1)) : type
1013
+ let sortedTypes = type
1014
+ const nullable = schema.nullable === true || type.includes('null')
1015
+
1016
+ if (nullable) {
1017
+ sortedTypes = sortedTypes.filter(type => type !== 'null')
1018
+ code += `
1019
+ if (${input} === null) {
1020
+ json += null
1021
+ } else {`
1022
+ }
1023
+
1314
1024
  sortedTypes.forEach((type, index) => {
1315
1025
  const statement = index === 0 ? 'if' : 'else if'
1316
1026
  const tempSchema = Object.assign({}, schema, { type })
1317
- const nestedResult = nested(laterCode, name, key, mergeLocation(location, { schema: tempSchema }), subKey, isArray)
1027
+ const nestedResult = buildValue(locationPath, input, mergeLocation(location, { schema: tempSchema }))
1318
1028
  switch (type) {
1319
1029
  case 'string': {
1320
1030
  code += `
1321
- ${statement}(obj${accessor} === null || typeof obj${accessor} === "${type}" || obj${accessor} instanceof Date || typeof obj${accessor}.toISOString === "function" || obj${accessor} instanceof RegExp || (typeof obj${accessor} === "object" && Object.hasOwnProperty.call(obj${accessor}, "toString")))
1322
- ${nestedResult.code}
1323
- `
1324
- break
1325
- }
1326
- case 'null': {
1327
- code += `
1328
- ${statement}(obj${accessor} == null)
1329
- ${nestedResult.code}
1031
+ ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof Date || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
1032
+ ${nestedResult}
1330
1033
  `
1331
1034
  break
1332
1035
  }
1333
1036
  case 'array': {
1334
1037
  code += `
1335
- ${statement}(Array.isArray(obj${accessor}))
1336
- ${nestedResult.code}
1038
+ ${statement}(Array.isArray(${input}))
1039
+ ${nestedResult}
1337
1040
  `
1338
1041
  break
1339
1042
  }
1340
1043
  case 'integer': {
1341
1044
  code += `
1342
- ${statement}(Number.isInteger(obj${accessor}) || obj${accessor} === null)
1343
- ${nestedResult.code}
1344
- `
1345
- break
1346
- }
1347
- case 'number': {
1348
- code += `
1349
- ${statement}(isNaN(obj${accessor}) === false)
1350
- ${nestedResult.code}
1045
+ ${statement}(Number.isInteger(${input}) || ${input} === null)
1046
+ ${nestedResult}
1351
1047
  `
1352
1048
  break
1353
1049
  }
1354
1050
  default: {
1355
1051
  code += `
1356
- ${statement}(typeof obj${accessor} === "${type}")
1357
- ${nestedResult.code}
1052
+ ${statement}(typeof ${input} === "${type}" || ${input} === null)
1053
+ ${nestedResult}
1358
1054
  `
1359
1055
  break
1360
1056
  }
1361
1057
  }
1362
- laterCode = nestedResult.laterCode
1363
1058
  })
1364
1059
  code += `
1365
- else json+= null
1060
+ else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
1366
1061
  `
1062
+
1063
+ if (nullable) {
1064
+ code += `
1065
+ }
1066
+ `
1067
+ }
1367
1068
  } else {
1368
1069
  throw new Error(`${type} unsupported`)
1369
1070
  }
1370
1071
  }
1371
1072
 
1372
- return {
1373
- code,
1374
- laterCode,
1375
- mapFnName: funcName
1073
+ return code
1074
+ }
1075
+
1076
+ function extendDateTimeType (schema) {
1077
+ if (schema.type === 'string' && ['date-time', 'date', 'time'].includes(schema.format)) {
1078
+ schema.fjs_date_type = schema.format
1079
+ delete schema.type
1080
+ delete schema.format
1081
+ }
1082
+ for (const property in schema) {
1083
+ if (typeof schema[property] === 'object') {
1084
+ extendDateTimeType(schema[property])
1085
+ }
1376
1086
  }
1377
1087
  }
1378
1088
 
@@ -1391,7 +1101,8 @@ module.exports = build
1391
1101
  module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
1392
1102
 
1393
1103
  module.exports.restore = function ({ code, ajv }) {
1104
+ const serializer = new Serializer()
1394
1105
  // eslint-disable-next-line
1395
- return (Function.apply(null, ['ajv', code])
1396
- .apply(null, [ajv]))
1106
+ return (Function.apply(null, ['ajv', 'serializer', code])
1107
+ .apply(null, [ajv, serializer]))
1397
1108
  }