fast-json-stringify 3.2.0 → 4.2.0

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