fast-json-stringify 5.9.2 → 5.11.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/README.md CHANGED
@@ -590,6 +590,8 @@ Otherwise, instead of raising an error, null values will be coerced as follows:
590
590
  - `number` -> `0`
591
591
  - `string` -> `""`
592
592
  - `boolean` -> `false`
593
+ - `object` -> `{}`
594
+ - `array` -> `[]`
593
595
 
594
596
  <a name="largearrays"></a>
595
597
  #### Large Arrays
package/index.js CHANGED
@@ -2,14 +2,13 @@
2
2
 
3
3
  /* eslint no-prototype-builtins: 0 */
4
4
 
5
- const merge = require('@fastify/deepmerge')()
6
- const clone = require('rfdc')({ proto: true })
7
5
  const { RefResolver } = require('json-schema-ref-resolver')
8
6
 
9
- const validate = require('./lib/schema-validator')
10
7
  const Serializer = require('./lib/serializer')
11
8
  const Validator = require('./lib/validator')
12
9
  const Location = require('./lib/location')
10
+ const validate = require('./lib/schema-validator')
11
+ const mergeSchemas = require('./lib/merge-schemas')
13
12
 
14
13
  const SINGLE_TICK = /'/g
15
14
 
@@ -28,10 +27,10 @@ const validLargeArrayMechanisms = [
28
27
  'json-stringify'
29
28
  ]
30
29
 
31
- const addComma = '!addComma && (addComma = true) || (json += \',\')'
32
-
33
30
  let schemaIdCounter = 0
34
31
 
32
+ const mergedSchemaRef = Symbol('fjs-merged-schema-ref')
33
+
35
34
  function isValidSchema (schema, name) {
36
35
  if (!validate(schema)) {
37
36
  if (name) {
@@ -46,13 +45,15 @@ function isValidSchema (schema, name) {
46
45
  }
47
46
  }
48
47
 
49
- function resolveRef (context, location, ref) {
48
+ function resolveRef (context, location) {
49
+ const ref = location.schema.$ref
50
+
50
51
  let hashIndex = ref.indexOf('#')
51
52
  if (hashIndex === -1) {
52
53
  hashIndex = ref.length
53
54
  }
54
55
 
55
- const schemaId = ref.slice(0, hashIndex) || location.getOriginSchemaId()
56
+ const schemaId = ref.slice(0, hashIndex) || location.schemaId
56
57
  const jsonPointer = ref.slice(hashIndex) || '#'
57
58
 
58
59
  const schema = context.refResolver.getSchema(schemaId, jsonPointer)
@@ -62,12 +63,17 @@ function resolveRef (context, location, ref) {
62
63
 
63
64
  const newLocation = new Location(schema, schemaId, jsonPointer)
64
65
  if (schema.$ref !== undefined) {
65
- return resolveRef(context, newLocation, schema.$ref)
66
+ return resolveRef(context, newLocation)
66
67
  }
67
68
 
68
69
  return newLocation
69
70
  }
70
71
 
72
+ function getMergedLocation (context, mergedSchemaId) {
73
+ const mergedSchema = context.refResolver.getSchema(mergedSchemaId, '#')
74
+ return new Location(mergedSchema, mergedSchemaId, '#')
75
+ }
76
+
71
77
  function getSchemaId (schema, rootSchemaId) {
72
78
  if (schema.$id && schema.$id.charAt(0) !== '#') {
73
79
  return schema.$id
@@ -85,7 +91,6 @@ function build (schema, options) {
85
91
  functionsCounter: 0,
86
92
  functionsNamesBySchema: new Map(),
87
93
  options,
88
- wrapObjects: true,
89
94
  refResolver: new RefResolver(),
90
95
  rootSchemaId: schema.$id || `__fjs_root_${schemaIdCounter++}`,
91
96
  validatorSchemasIds: new Set()
@@ -255,7 +260,7 @@ function inferTypeByKeyword (schema) {
255
260
  return schema.type
256
261
  }
257
262
 
258
- function buildExtraObjectPropertiesSerializer (context, location) {
263
+ function buildExtraObjectPropertiesSerializer (context, location, addComma) {
259
264
  const schema = location.schema
260
265
  const propertiesKeys = Object.keys(schema.properties || {})
261
266
 
@@ -314,225 +319,137 @@ function buildExtraObjectPropertiesSerializer (context, location) {
314
319
  }
315
320
 
316
321
  function buildInnerObject (context, location) {
317
- let code = ''
318
322
  const schema = location.schema
319
- const required = schema.required || []
320
323
 
321
324
  const propertiesLocation = location.getPropertyLocation('properties')
325
+ const requiredProperties = schema.required || []
322
326
 
323
- const requiredWithDefault = []
324
- const requiredWithoutDefault = []
325
- if (schema.properties) {
326
- for (const key of Object.keys(schema.properties)) {
327
- if (required.indexOf(key) === -1) {
328
- continue
329
- }
330
- let propertyLocation = propertiesLocation.getPropertyLocation(key)
331
- if (propertyLocation.schema.$ref) {
332
- propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
333
- }
327
+ // Should serialize required properties first
328
+ const propertiesKeys = Object.keys(schema.properties || {}).sort(
329
+ (key1, key2) => {
330
+ const required1 = requiredProperties.includes(key1)
331
+ const required2 = requiredProperties.includes(key2)
332
+ return required1 === required2 ? 0 : required1 ? -1 : 1
333
+ }
334
+ )
335
+ const hasRequiredProperties = requiredProperties.includes(propertiesKeys[0])
334
336
 
335
- const sanitizedKey = JSON.stringify(key)
337
+ let code = ''
336
338
 
337
- // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
338
- // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
339
- const defaultValue = propertyLocation.schema.default
340
- if (defaultValue === undefined) {
341
- code += `if (obj[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey} is required!')\n`
342
- requiredWithoutDefault.push(key)
343
- }
344
- requiredWithDefault.push(key)
339
+ for (const key of requiredProperties) {
340
+ if (!propertiesKeys.includes(key)) {
341
+ code += `if (obj['${key}'] === undefined) throw new Error('"${key}" is required!')\n`
345
342
  }
346
343
  }
347
344
 
348
- // handle extraneous required fields
349
- for (const requiredProperty of required) {
350
- if (requiredWithDefault.indexOf(requiredProperty) !== -1) continue
351
- code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
345
+ code += 'let json = \'{\'\n'
346
+
347
+ let addComma = ''
348
+ if (!hasRequiredProperties) {
349
+ code += 'let addComma = false\n'
350
+ addComma = '!addComma && (addComma = true) || (json += \',\')'
352
351
  }
353
352
 
354
- code += `
355
- let addComma = false
356
- let json = '${context.wrapObjects ? '{' : ''}'
357
- `
358
- const wrapObjects = context.wrapObjects
359
- context.wrapObjects = true
360
-
361
- if (schema.properties) {
362
- for (const key of Object.keys(schema.properties)) {
363
- let propertyLocation = propertiesLocation.getPropertyLocation(key)
364
- if (propertyLocation.schema.$ref) {
365
- propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
366
- }
353
+ for (const key of propertiesKeys) {
354
+ let propertyLocation = propertiesLocation.getPropertyLocation(key)
355
+ if (propertyLocation.schema.$ref) {
356
+ propertyLocation = resolveRef(context, propertyLocation)
357
+ }
367
358
 
368
- const sanitizedKey = JSON.stringify(key)
359
+ const sanitizedKey = JSON.stringify(key)
360
+ const defaultValue = propertyLocation.schema.default
361
+ const isRequired = requiredProperties.includes(key)
369
362
 
370
- if (requiredWithoutDefault.indexOf(key) !== -1) {
371
- code += `
363
+ code += `
364
+ if (obj[${sanitizedKey}] !== undefined) {
372
365
  ${addComma}
373
366
  json += ${JSON.stringify(sanitizedKey + ':')}
374
367
  ${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
368
+ }`
369
+
370
+ if (defaultValue !== undefined) {
371
+ code += ` else {
372
+ ${addComma}
373
+ json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
374
+ }
375
375
  `
376
- } else {
377
- // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
378
- // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
379
- code += `
380
- if (obj[${sanitizedKey}] !== undefined) {
381
- ${addComma}
382
- json += ${JSON.stringify(sanitizedKey + ':')}
383
- ${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
384
- }
385
- `
386
- const defaultValue = propertyLocation.schema.default
387
- if (defaultValue !== undefined) {
388
- code += `
389
- else {
390
- ${addComma}
391
- json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
392
- }
393
- `
394
- }
376
+ } else if (isRequired) {
377
+ code += ` else {
378
+ throw new Error('${sanitizedKey} is required!')
395
379
  }
380
+ `
381
+ } else {
382
+ code += '\n'
383
+ }
384
+
385
+ if (hasRequiredProperties) {
386
+ addComma = 'json += \',\''
396
387
  }
397
388
  }
398
389
 
399
390
  if (schema.patternProperties || schema.additionalProperties) {
400
- code += buildExtraObjectPropertiesSerializer(context, location)
391
+ code += buildExtraObjectPropertiesSerializer(context, location, addComma)
401
392
  }
402
393
 
403
- context.wrapObjects = wrapObjects
404
394
  code += `
405
- return json${context.wrapObjects ? ' + \'}\'' : ''}
395
+ return json + '}'
406
396
  `
407
397
  return code
408
398
  }
409
399
 
410
- function mergeAllOfSchema (context, location, schema, mergedSchema) {
411
- const allOfLocation = location.getPropertyLocation('allOf')
412
-
413
- for (let i = 0; i < schema.allOf.length; i++) {
414
- let allOfSchema = schema.allOf[i]
415
-
416
- if (allOfSchema.$ref) {
417
- const allOfSchemaLocation = allOfLocation.getPropertyLocation(i)
418
- allOfSchema = resolveRef(context, allOfSchemaLocation, allOfSchema.$ref).schema
400
+ function mergeLocations (context, mergedSchemaId, mergedLocations) {
401
+ for (let i = 0; i < mergedLocations.length; i++) {
402
+ const location = mergedLocations[i]
403
+ const schema = location.schema
404
+ if (schema.$ref) {
405
+ mergedLocations[i] = resolveRef(context, location)
419
406
  }
407
+ }
420
408
 
421
- let allOfSchemaType = allOfSchema.type
422
- if (allOfSchemaType === undefined) {
423
- allOfSchemaType = inferTypeByKeyword(allOfSchema)
424
- }
409
+ const mergedSchemas = []
410
+ for (const location of mergedLocations) {
411
+ const schema = cloneOriginSchema(location.schema, location.schemaId)
412
+ delete schema.$id
425
413
 
426
- if (allOfSchemaType !== undefined) {
427
- if (
428
- mergedSchema.type !== undefined &&
429
- mergedSchema.type !== allOfSchemaType
430
- ) {
431
- throw new Error('allOf schemas have different type values')
432
- }
433
- mergedSchema.type = allOfSchemaType
434
- }
414
+ mergedSchemas.push(schema)
415
+ }
435
416
 
436
- if (allOfSchema.format !== undefined) {
437
- if (
438
- mergedSchema.format !== undefined &&
439
- mergedSchema.format !== allOfSchema.format
440
- ) {
441
- throw new Error('allOf schemas have different format values')
442
- }
443
- mergedSchema.format = allOfSchema.format
444
- }
417
+ const mergedSchema = mergeSchemas(mergedSchemas)
418
+ const mergedLocation = new Location(mergedSchema, mergedSchemaId)
445
419
 
446
- if (allOfSchema.nullable !== undefined) {
447
- if (
448
- mergedSchema.nullable !== undefined &&
449
- mergedSchema.nullable !== allOfSchema.nullable
450
- ) {
451
- throw new Error('allOf schemas have different nullable values')
452
- }
453
- mergedSchema.nullable = allOfSchema.nullable
454
- }
420
+ context.refResolver.addSchema(mergedSchema, mergedSchemaId)
421
+ return mergedLocation
422
+ }
455
423
 
456
- if (allOfSchema.properties !== undefined) {
457
- if (mergedSchema.properties === undefined) {
458
- mergedSchema.properties = {}
459
- }
460
- Object.assign(mergedSchema.properties, allOfSchema.properties)
461
- }
424
+ function cloneOriginSchema (schema, schemaId) {
425
+ const clonedSchema = Array.isArray(schema) ? [] : {}
462
426
 
463
- if (allOfSchema.additionalProperties !== undefined) {
464
- if (mergedSchema.additionalProperties === undefined) {
465
- mergedSchema.additionalProperties = {}
466
- }
467
- Object.assign(mergedSchema.additionalProperties, allOfSchema.additionalProperties)
468
- }
427
+ if (
428
+ schema.$id !== undefined &&
429
+ schema.$id.charAt(0) !== '#'
430
+ ) {
431
+ schemaId = schema.$id
432
+ }
469
433
 
470
- if (allOfSchema.patternProperties !== undefined) {
471
- if (mergedSchema.patternProperties === undefined) {
472
- mergedSchema.patternProperties = {}
473
- }
474
- Object.assign(mergedSchema.patternProperties, allOfSchema.patternProperties)
475
- }
434
+ if (schema[mergedSchemaRef]) {
435
+ clonedSchema[mergedSchemaRef] = schema[mergedSchemaRef]
436
+ }
476
437
 
477
- if (allOfSchema.required !== undefined) {
478
- if (mergedSchema.required === undefined) {
479
- mergedSchema.required = []
480
- }
481
- mergedSchema.required.push(...allOfSchema.required)
482
- }
438
+ for (const key in schema) {
439
+ let value = schema[key]
483
440
 
484
- if (allOfSchema.oneOf !== undefined) {
485
- if (mergedSchema.oneOf === undefined) {
486
- mergedSchema.oneOf = []
487
- }
488
- mergedSchema.oneOf.push(...allOfSchema.oneOf)
441
+ if (key === '$ref' && value.charAt(0) === '#') {
442
+ value = schemaId + value
489
443
  }
490
444
 
491
- if (allOfSchema.anyOf !== undefined) {
492
- if (mergedSchema.anyOf === undefined) {
493
- mergedSchema.anyOf = []
494
- }
495
- mergedSchema.anyOf.push(...allOfSchema.anyOf)
445
+ if (typeof value === 'object' && value !== null) {
446
+ value = cloneOriginSchema(value, schemaId)
496
447
  }
497
448
 
498
- if (allOfSchema.allOf !== undefined) {
499
- mergeAllOfSchema(context, location, allOfSchema, mergedSchema)
500
- }
449
+ clonedSchema[key] = value
501
450
  }
502
- delete mergedSchema.allOf
503
-
504
- mergedSchema.$id = `__fjs_merged_${schemaIdCounter++}`
505
- context.refResolver.addSchema(mergedSchema)
506
- location.addMergedSchema(mergedSchema, mergedSchema.$id)
507
- }
508
-
509
- function addIfThenElse (context, location, input) {
510
- context.validatorSchemasIds.add(location.getSchemaId())
511
-
512
- const schema = merge({}, location.schema)
513
- const thenSchema = schema.then
514
- const elseSchema = schema.else || { additionalProperties: true }
515
451
 
516
- delete schema.if
517
- delete schema.then
518
- delete schema.else
519
-
520
- const ifLocation = location.getPropertyLocation('if')
521
- const ifSchemaRef = ifLocation.getSchemaRef()
522
-
523
- const thenLocation = location.getPropertyLocation('then')
524
- thenLocation.schema = merge(schema, thenSchema)
525
-
526
- const elseLocation = location.getPropertyLocation('else')
527
- elseLocation.schema = merge(schema, elseSchema)
528
-
529
- return `
530
- if (validator.validate("${ifSchemaRef}", ${input})) {
531
- ${buildValue(context, thenLocation, input)}
532
- } else {
533
- ${buildValue(context, elseLocation, input)}
534
- }
535
- `
452
+ return clonedSchema
536
453
  }
537
454
 
538
455
  function toJSON (variableName) {
@@ -560,10 +477,13 @@ function buildObject (context, location) {
560
477
  let functionCode = `
561
478
  `
562
479
 
480
+ const nullable = schema.nullable === true
563
481
  functionCode += `
564
482
  // ${schemaRef}
565
483
  function ${functionName} (input) {
566
484
  const obj = ${toJSON('input')}
485
+ ${!nullable ? 'if (obj === null) return \'{}\'' : ''}
486
+
567
487
  ${buildInnerObject(context, location)}
568
488
  }
569
489
  `
@@ -579,7 +499,7 @@ function buildArray (context, location) {
579
499
  itemsLocation.schema = itemsLocation.schema || {}
580
500
 
581
501
  if (itemsLocation.schema.$ref) {
582
- itemsLocation = resolveRef(context, itemsLocation, itemsLocation.schema.$ref)
502
+ itemsLocation = resolveRef(context, itemsLocation)
583
503
  }
584
504
 
585
505
  const itemsSchema = itemsLocation.schema
@@ -601,7 +521,9 @@ function buildArray (context, location) {
601
521
  // ${schemaRef}
602
522
  `
603
523
 
524
+ const nullable = schema.nullable === true
604
525
  functionCode += `
526
+ ${!nullable ? 'if (obj === null) return \'[]\'' : ''}
605
527
  if (!Array.isArray(obj)) {
606
528
  throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
607
529
  }
@@ -855,81 +777,193 @@ function buildConstSerializer (location, input) {
855
777
  return code
856
778
  }
857
779
 
858
- function buildValue (context, location, input) {
859
- let schema = location.schema
780
+ function buildAllOf (context, location, input) {
781
+ const schema = location.schema
860
782
 
861
- if (typeof schema === 'boolean') {
862
- return `json += JSON.stringify(${input})`
783
+ let mergedSchemaId = schema[mergedSchemaRef]
784
+ if (mergedSchemaId) {
785
+ const mergedLocation = getMergedLocation(context, mergedSchemaId)
786
+ return buildValue(context, mergedLocation, input)
863
787
  }
864
788
 
865
- if (schema.$ref) {
866
- location = resolveRef(context, location, schema.$ref)
867
- schema = location.schema
789
+ mergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
790
+ schema[mergedSchemaRef] = mergedSchemaId
791
+
792
+ const { allOf, ...schemaWithoutAllOf } = location.schema
793
+ const locations = [
794
+ new Location(
795
+ schemaWithoutAllOf,
796
+ location.schemaId,
797
+ location.jsonPointer
798
+ )
799
+ ]
800
+
801
+ const allOfsLocation = location.getPropertyLocation('allOf')
802
+ for (let i = 0; i < allOf.length; i++) {
803
+ locations.push(allOfsLocation.getPropertyLocation(i))
868
804
  }
869
805
 
870
- if (schema.type === undefined) {
871
- const inferredType = inferTypeByKeyword(schema)
872
- if (inferredType) {
873
- schema.type = inferredType
806
+ const mergedLocation = mergeLocations(context, mergedSchemaId, locations)
807
+ return buildValue(context, mergedLocation, input)
808
+ }
809
+
810
+ function buildOneOf (context, location, input) {
811
+ context.validatorSchemasIds.add(location.schemaId)
812
+
813
+ const schema = location.schema
814
+
815
+ const type = schema.anyOf ? 'anyOf' : 'oneOf'
816
+ const { [type]: oneOfs, ...schemaWithoutAnyOf } = location.schema
817
+
818
+ const locationWithoutOneOf = new Location(
819
+ schemaWithoutAnyOf,
820
+ location.schemaId,
821
+ location.jsonPointer
822
+ )
823
+ const oneOfsLocation = location.getPropertyLocation(type)
824
+
825
+ let code = ''
826
+
827
+ for (let index = 0; index < oneOfs.length; index++) {
828
+ const optionLocation = oneOfsLocation.getPropertyLocation(index)
829
+ const optionSchema = optionLocation.schema
830
+
831
+ let mergedSchemaId = optionSchema[mergedSchemaRef]
832
+ let mergedLocation = null
833
+ if (mergedSchemaId) {
834
+ mergedLocation = getMergedLocation(context, mergedSchemaId)
835
+ } else {
836
+ mergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
837
+ optionSchema[mergedSchemaRef] = mergedSchemaId
838
+
839
+ mergedLocation = mergeLocations(context, mergedSchemaId, [
840
+ locationWithoutOneOf,
841
+ optionLocation
842
+ ])
874
843
  }
844
+
845
+ const nestedResult = buildValue(context, mergedLocation, input)
846
+ const schemaRef = optionLocation.getSchemaRef()
847
+ code += `
848
+ ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
849
+ ${nestedResult}
850
+ `
875
851
  }
876
852
 
877
- if (schema.if && schema.then) {
878
- return addIfThenElse(context, location, input)
853
+ let schemaRef = location.getSchemaRef()
854
+ if (schemaRef.startsWith(context.rootSchemaId)) {
855
+ schemaRef = schemaRef.replace(context.rootSchemaId, '')
879
856
  }
880
857
 
881
- if (schema.allOf) {
882
- mergeAllOfSchema(context, location, schema, clone(schema))
883
- schema = location.schema
858
+ code += `
859
+ else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
860
+ `
861
+
862
+ return code
863
+ }
864
+
865
+ function buildIfThenElse (context, location, input) {
866
+ context.validatorSchemasIds.add(location.schemaId)
867
+
868
+ const {
869
+ if: ifSchema,
870
+ then: thenSchema,
871
+ else: elseSchema,
872
+ ...schemaWithoutIfThenElse
873
+ } = location.schema
874
+
875
+ const rootLocation = new Location(
876
+ schemaWithoutIfThenElse,
877
+ location.schemaId,
878
+ location.jsonPointer
879
+ )
880
+
881
+ const ifLocation = location.getPropertyLocation('if')
882
+ const ifSchemaRef = ifLocation.getSchemaRef()
883
+
884
+ const thenLocation = location.getPropertyLocation('then')
885
+ let thenMergedSchemaId = thenSchema[mergedSchemaRef]
886
+ let thenMergedLocation = null
887
+ if (thenMergedSchemaId) {
888
+ thenMergedLocation = getMergedLocation(context, thenMergedSchemaId)
889
+ } else {
890
+ thenMergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
891
+ thenSchema[mergedSchemaRef] = thenMergedSchemaId
892
+
893
+ thenMergedLocation = mergeLocations(context, thenMergedSchemaId, [
894
+ rootLocation,
895
+ thenLocation
896
+ ])
884
897
  }
885
898
 
886
- const type = schema.type
899
+ if (!elseSchema) {
900
+ return `
901
+ if (validator.validate("${ifSchemaRef}", ${input})) {
902
+ ${buildValue(context, thenMergedLocation, input)}
903
+ } else {
904
+ ${buildValue(context, rootLocation, input)}
905
+ }
906
+ `
907
+ }
887
908
 
888
- let code = ''
909
+ const elseLocation = location.getPropertyLocation('else')
910
+ let elseMergedSchemaId = elseSchema[mergedSchemaRef]
911
+ let elseMergedLocation = null
912
+ if (elseMergedSchemaId) {
913
+ elseMergedLocation = getMergedLocation(context, elseMergedSchemaId)
914
+ } else {
915
+ elseMergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
916
+ elseSchema[mergedSchemaRef] = elseMergedSchemaId
889
917
 
890
- if ((type === undefined || type === 'object') && (schema.anyOf || schema.oneOf)) {
891
- context.validatorSchemasIds.add(location.getSchemaId())
918
+ elseMergedLocation = mergeLocations(context, elseMergedSchemaId, [
919
+ rootLocation,
920
+ elseLocation
921
+ ])
922
+ }
892
923
 
893
- if (schema.type === 'object') {
894
- context.wrapObjects = false
895
- const funcName = buildObject(context, location)
896
- code += `
897
- json += '{'
898
- json += ${funcName}(${input})
899
- json += ','
900
- `
924
+ return `
925
+ if (validator.validate("${ifSchemaRef}", ${input})) {
926
+ ${buildValue(context, thenMergedLocation, input)}
927
+ } else {
928
+ ${buildValue(context, elseMergedLocation, input)}
901
929
  }
930
+ `
931
+ }
902
932
 
903
- const type = schema.anyOf ? 'anyOf' : 'oneOf'
904
- const anyOfLocation = location.getPropertyLocation(type)
933
+ function buildValue (context, location, input) {
934
+ let schema = location.schema
905
935
 
906
- for (let index = 0; index < location.schema[type].length; index++) {
907
- const optionLocation = anyOfLocation.getPropertyLocation(index)
908
- const schemaRef = optionLocation.getSchemaRef()
909
- const nestedResult = buildValue(context, optionLocation, input)
910
- code += `
911
- ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
912
- ${nestedResult}
913
- `
914
- }
936
+ if (typeof schema === 'boolean') {
937
+ return `json += JSON.stringify(${input})`
938
+ }
915
939
 
916
- let schemaRef = location.getSchemaRef()
917
- if (schemaRef.startsWith(context.rootSchemaId)) {
918
- schemaRef = schemaRef.replace(context.rootSchemaId, '')
919
- }
940
+ if (schema.$ref) {
941
+ location = resolveRef(context, location)
942
+ schema = location.schema
943
+ }
920
944
 
921
- code += `
922
- else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
923
- `
924
- if (schema.type === 'object') {
925
- code += `
926
- json += '}'
927
- `
928
- context.wrapObjects = true
945
+ if (schema.allOf) {
946
+ return buildAllOf(context, location, input)
947
+ }
948
+
949
+ if (schema.anyOf || schema.oneOf) {
950
+ return buildOneOf(context, location, input)
951
+ }
952
+
953
+ if (schema.if && schema.then) {
954
+ return buildIfThenElse(context, location, input)
955
+ }
956
+
957
+ if (schema.type === undefined) {
958
+ const inferredType = inferTypeByKeyword(schema)
959
+ if (inferredType) {
960
+ schema.type = inferredType
929
961
  }
930
- return code
931
962
  }
932
963
 
964
+ let code = ''
965
+
966
+ const type = schema.type
933
967
  const nullable = schema.nullable === true
934
968
  if (nullable) {
935
969
  code += `
package/lib/location.js CHANGED
@@ -5,7 +5,6 @@ class Location {
5
5
  this.schema = schema
6
6
  this.schemaId = schemaId
7
7
  this.jsonPointer = jsonPointer
8
- this.mergedSchemaId = null
9
8
  }
10
9
 
11
10
  getPropertyLocation (propertyName) {
@@ -14,39 +13,11 @@ class Location {
14
13
  this.schemaId,
15
14
  this.jsonPointer + '/' + propertyName
16
15
  )
17
-
18
- if (this.mergedSchemaId !== null) {
19
- propertyLocation.addMergedSchema(
20
- this.schema[propertyName],
21
- this.mergedSchemaId,
22
- this.jsonPointer + '/' + propertyName
23
- )
24
- }
25
-
26
16
  return propertyLocation
27
17
  }
28
18
 
29
- // Use this method to get current schema location.
30
- // Use it when you need to create reference to the current location.
31
- getSchemaId () {
32
- return this.mergedSchemaId || this.schemaId
33
- }
34
-
35
- // Use this method to get original schema id for resolving user schema $refs
36
- // Don't join it with a JSON pointer to get the current location.
37
- getOriginSchemaId () {
38
- return this.schemaId
39
- }
40
-
41
19
  getSchemaRef () {
42
- const schemaId = this.getSchemaId()
43
- return schemaId + this.jsonPointer
44
- }
45
-
46
- addMergedSchema (mergedSchema, schemaId, jsonPointer = '#') {
47
- this.schema = mergedSchema
48
- this.mergedSchemaId = schemaId
49
- this.jsonPointer = jsonPointer
20
+ return this.schemaId + this.jsonPointer
50
21
  }
51
22
  }
52
23
 
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ const { mergeSchemas: _mergeSchemas } = require('@fastify/merge-json-schemas')
4
+
5
+ function mergeSchemas (schemas) {
6
+ return _mergeSchemas(schemas, { onConflict: 'skip' })
7
+ }
8
+
9
+ module.exports = mergeSchemas
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.9.2",
3
+ "version": "5.11.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -46,18 +46,18 @@
46
46
  "simple-git": "^3.7.1",
47
47
  "standard": "^17.0.0",
48
48
  "tap": "^16.0.1",
49
- "tsd": "^0.29.0",
49
+ "tsd": "^0.30.3",
50
50
  "webpack": "^5.40.0",
51
51
  "fast-json-stringify": "."
52
52
  },
53
53
  "dependencies": {
54
- "@fastify/deepmerge": "^1.0.0",
55
54
  "ajv": "^8.10.0",
56
55
  "ajv-formats": "^2.1.1",
57
56
  "fast-deep-equal": "^3.1.3",
58
57
  "fast-uri": "^2.1.0",
59
58
  "rfdc": "^1.2.0",
60
- "json-schema-ref-resolver": "^1.0.1"
59
+ "json-schema-ref-resolver": "^1.0.1",
60
+ "@fastify/merge-json-schemas": "^0.1.0"
61
61
  },
62
62
  "standard": {
63
63
  "ignore": [
@@ -121,7 +121,7 @@ test('object with allOf and multiple schema on the allOf', (t) => {
121
121
  id: 1,
122
122
  name: 'string',
123
123
  tag: 'otherString'
124
- }), '{"name":"string","tag":"otherString","id":1}')
124
+ }), '{"name":"string","id":1,"tag":"otherString"}')
125
125
  })
126
126
 
127
127
  test('object with allOf and one schema on the allOf', (t) => {
@@ -553,8 +553,60 @@ test('allof with local anchor reference', (t) => {
553
553
  t.equal(stringify(data), JSON.stringify(data))
554
554
  })
555
555
 
556
+ test('allOf: multiple nested $ref properties', (t) => {
557
+ t.plan(2)
558
+
559
+ const externalSchema1 = {
560
+ $id: 'externalSchema1',
561
+ oneOf: [
562
+ { $ref: '#/definitions/id1' }
563
+ ],
564
+ definitions: {
565
+ id1: {
566
+ type: 'object',
567
+ properties: {
568
+ id1: {
569
+ type: 'integer'
570
+ }
571
+ },
572
+ additionalProperties: false
573
+ }
574
+ }
575
+ }
576
+
577
+ const externalSchema2 = {
578
+ $id: 'externalSchema2',
579
+ oneOf: [
580
+ { $ref: '#/definitions/id2' }
581
+ ],
582
+ definitions: {
583
+ id2: {
584
+ type: 'object',
585
+ properties: {
586
+ id2: {
587
+ type: 'integer'
588
+ }
589
+ },
590
+ additionalProperties: false
591
+ }
592
+ }
593
+ }
594
+
595
+ const schema = {
596
+ anyOf: [
597
+ { $ref: 'externalSchema1' },
598
+ { $ref: 'externalSchema2' }
599
+ ]
600
+ }
601
+
602
+ const stringify = build(schema, { schema: [externalSchema1, externalSchema2] })
603
+
604
+ t.equal(stringify({ id1: 1 }), JSON.stringify({ id1: 1 }))
605
+ t.equal(stringify({ id2: 2 }), JSON.stringify({ id2: 2 }))
606
+ })
607
+
556
608
  test('allOf: throw Error if types mismatch ', (t) => {
557
- t.plan(1)
609
+ t.plan(3)
558
610
 
559
611
  const schema = {
560
612
  allOf: [
@@ -562,11 +614,18 @@ test('allOf: throw Error if types mismatch ', (t) => {
562
614
  { type: 'number' }
563
615
  ]
564
616
  }
565
- t.throws(() => build(schema), new Error('allOf schemas have different type values'))
617
+ try {
618
+ build(schema)
619
+ t.fail('should throw the MergeError')
620
+ } catch (error) {
621
+ t.ok(error instanceof Error)
622
+ t.equal(error.message, 'Failed to merge "type" keyword schemas.')
623
+ t.same(error.schemas, [['string'], ['number']])
624
+ }
566
625
  })
567
626
 
568
627
  test('allOf: throw Error if format mismatch ', (t) => {
569
- t.plan(1)
628
+ t.plan(3)
570
629
 
571
630
  const schema = {
572
631
  allOf: [
@@ -574,29 +633,87 @@ test('allOf: throw Error if format mismatch ', (t) => {
574
633
  { format: 'time' }
575
634
  ]
576
635
  }
577
- t.throws(() => build(schema), new Error('allOf schemas have different format values'))
636
+ try {
637
+ build(schema)
638
+ t.fail('should throw the MergeError')
639
+ } catch (error) {
640
+ t.ok(error instanceof Error)
641
+ t.equal(error.message, 'Failed to merge "format" keyword schemas.')
642
+ t.same(error.schemas, ['date', 'time'])
643
+ }
578
644
  })
579
645
 
580
- test('allOf: throw Error if nullable mismatch /1', (t) => {
646
+ test('recursive nested allOfs', (t) => {
581
647
  t.plan(1)
582
648
 
583
649
  const schema = {
584
- allOf: [
585
- { nullable: true },
586
- { nullable: false }
587
- ]
650
+ type: 'object',
651
+ properties: {
652
+ foo: {
653
+ additionalProperties: false,
654
+ allOf: [{ $ref: '#' }]
655
+ }
656
+ }
588
657
  }
589
- t.throws(() => build(schema), new Error('allOf schemas have different nullable values'))
658
+
659
+ const data = { foo: {} }
660
+ const stringify = build(schema)
661
+ t.equal(stringify(data), JSON.stringify(data))
590
662
  })
591
663
 
592
- test('allOf: throw Error if nullable mismatch /2', (t) => {
664
+ test('recursive nested allOfs', (t) => {
593
665
  t.plan(1)
594
666
 
595
667
  const schema = {
596
- allOf: [
597
- { nullable: false },
598
- { nullable: true }
599
- ]
668
+ type: 'object',
669
+ properties: {
670
+ foo: {
671
+ additionalProperties: false,
672
+ allOf: [{ allOf: [{ $ref: '#' }] }]
673
+ }
674
+ }
675
+ }
676
+
677
+ const data = { foo: {} }
678
+ const stringify = build(schema)
679
+ t.equal(stringify(data), JSON.stringify(data))
680
+ })
681
+
682
+ test('external recursive allOfs', (t) => {
683
+ t.plan(1)
684
+
685
+ const externalSchema = {
686
+ type: 'object',
687
+ properties: {
688
+ foo: {
689
+ properties: {
690
+ bar: { type: 'string' }
691
+ },
692
+ allOf: [{ $ref: '#' }]
693
+ }
694
+ }
695
+ }
696
+
697
+ const schema = {
698
+ type: 'object',
699
+ properties: {
700
+ a: { $ref: 'externalSchema#/properties/foo' },
701
+ b: { $ref: 'externalSchema#/properties/foo' }
702
+ }
703
+ }
704
+
705
+ const data = {
706
+ a: {
707
+ foo: {},
708
+ bar: '42',
709
+ baz: 42
710
+ },
711
+ b: {
712
+ foo: {},
713
+ bar: '42',
714
+ baz: 42
715
+ }
600
716
  }
601
- t.throws(() => build(schema), new Error('allOf schemas have different nullable values'))
717
+ const stringify = build(schema, { schema: { externalSchema } })
718
+ t.equal(stringify(data), '{"a":{"bar":"42","foo":{}},"b":{"bar":"42","foo":{}}}')
602
719
  })
@@ -644,3 +644,124 @@ test('object with ref and validated properties', (t) => {
644
644
  const stringify = build(schema, { schema: externalSchemas })
645
645
  t.equal(stringify({ id: 1, reference: 'hi' }), '{"id":1,"reference":"hi"}')
646
646
  })
647
+
648
+ test('anyOf required props', (t) => {
649
+ t.plan(3)
650
+
651
+ const schema = {
652
+ type: 'object',
653
+ properties: {
654
+ prop1: { type: 'string' },
655
+ prop2: { type: 'string' },
656
+ prop3: { type: 'string' }
657
+ },
658
+ required: ['prop1'],
659
+ anyOf: [{ required: ['prop2'] }, { required: ['prop3'] }]
660
+ }
661
+ const stringify = build(schema)
662
+ t.equal(stringify({ prop1: 'test', prop2: 'test2' }), '{"prop1":"test","prop2":"test2"}')
663
+ t.equal(stringify({ prop1: 'test', prop3: 'test3' }), '{"prop1":"test","prop3":"test3"}')
664
+ t.equal(stringify({ prop1: 'test', prop2: 'test2', prop3: 'test3' }), '{"prop1":"test","prop2":"test2","prop3":"test3"}')
665
+ })
666
+
667
+ test('anyOf required props', (t) => {
668
+ t.plan(3)
669
+
670
+ const schema = {
671
+ type: 'object',
672
+ properties: {
673
+ prop1: { type: 'string' }
674
+ },
675
+ anyOf: [
676
+ {
677
+ properties: {
678
+ prop2: { type: 'string' }
679
+ }
680
+ },
681
+ {
682
+ properties: {
683
+ prop3: { type: 'string' }
684
+ }
685
+ }
686
+ ]
687
+ }
688
+ const stringify = build(schema)
689
+ t.equal(stringify({ prop1: 'test1' }), '{"prop1":"test1"}')
690
+ t.equal(stringify({ prop2: 'test2' }), '{"prop2":"test2"}')
691
+ t.equal(stringify({ prop1: 'test1', prop2: 'test2' }), '{"prop1":"test1","prop2":"test2"}')
692
+ })
693
+
694
+ test('recursive nested anyOfs', (t) => {
695
+ t.plan(1)
696
+
697
+ const schema = {
698
+ type: 'object',
699
+ properties: {
700
+ foo: {
701
+ additionalProperties: false,
702
+ anyOf: [{ $ref: '#' }]
703
+ }
704
+ }
705
+ }
706
+
707
+ const data = { foo: {} }
708
+ const stringify = build(schema)
709
+ t.equal(stringify(data), JSON.stringify(data))
710
+ })
711
+
712
+ test('recursive nested anyOfs', (t) => {
713
+ t.plan(1)
714
+
715
+ const schema = {
716
+ type: 'object',
717
+ properties: {
718
+ foo: {
719
+ additionalProperties: false,
720
+ anyOf: [{ anyOf: [{ $ref: '#' }] }]
721
+ }
722
+ }
723
+ }
724
+
725
+ const data = { foo: {} }
726
+ const stringify = build(schema)
727
+ t.equal(stringify(data), JSON.stringify(data))
728
+ })
729
+
730
+ test('external recursive anyOfs', (t) => {
731
+ t.plan(1)
732
+
733
+ const externalSchema = {
734
+ type: 'object',
735
+ properties: {
736
+ foo: {
737
+ properties: {
738
+ bar: { type: 'string' }
739
+ },
740
+ anyOf: [{ $ref: '#' }]
741
+ }
742
+ }
743
+ }
744
+
745
+ const schema = {
746
+ type: 'object',
747
+ properties: {
748
+ a: { $ref: 'externalSchema#/properties/foo' },
749
+ b: { $ref: 'externalSchema#/properties/foo' }
750
+ }
751
+ }
752
+
753
+ const data = {
754
+ a: {
755
+ foo: {},
756
+ bar: '42',
757
+ baz: 42
758
+ },
759
+ b: {
760
+ foo: {},
761
+ bar: '42',
762
+ baz: 42
763
+ }
764
+ }
765
+ const stringify = build(schema, { schema: { externalSchema } })
766
+ t.equal(stringify(data), '{"a":{"bar":"42","foo":{}},"b":{"bar":"42","foo":{}}}')
767
+ })
@@ -249,15 +249,7 @@ const alphabetOutput = JSON.stringify({
249
249
  const deepFoobarOutput = JSON.stringify({
250
250
  foobar: JSON.parse(foobarOutput)
251
251
  })
252
- const noElseGreetingOutput = JSON.stringify({
253
- kind: 'greeting',
254
- foo: 'FOO',
255
- bar: 42,
256
- hi: 'HI',
257
- hello: 45,
258
- a: 'A',
259
- b: 35
260
- })
252
+ const noElseGreetingOutput = JSON.stringify({})
261
253
 
262
254
  t.test('if-then-else', t => {
263
255
  const tests = [
@@ -419,3 +411,60 @@ t.test('if/else with array', (t) => {
419
411
  t.equal(stringify(['1']), JSON.stringify(['1']))
420
412
  t.equal(stringify(['1', '2']), JSON.stringify([1, 2]))
421
413
  })
414
+
415
+ t.test('external recursive if/then/else', (t) => {
416
+ t.plan(1)
417
+
418
+ const externalSchema = {
419
+ type: 'object',
420
+ properties: {
421
+ base: { type: 'string' },
422
+ self: { $ref: 'externalSchema#' }
423
+ },
424
+ if: {
425
+ type: 'object',
426
+ properties: {
427
+ foo: { type: 'string', const: '41' }
428
+ }
429
+ },
430
+ then: {
431
+ type: 'object',
432
+ properties: {
433
+ bar: { type: 'string', const: '42' }
434
+ }
435
+ },
436
+ else: {
437
+ type: 'object',
438
+ properties: {
439
+ baz: { type: 'string', const: '43' }
440
+ }
441
+ }
442
+ }
443
+
444
+ const schema = {
445
+ type: 'object',
446
+ properties: {
447
+ a: { $ref: 'externalSchema#/properties/self' },
448
+ b: { $ref: 'externalSchema#/properties/self' }
449
+ }
450
+ }
451
+
452
+ const data = {
453
+ a: {
454
+ base: 'a',
455
+ foo: '41',
456
+ bar: '42',
457
+ baz: '43',
458
+ ignore: 'ignored'
459
+ },
460
+ b: {
461
+ base: 'b',
462
+ foo: 'not-41',
463
+ bar: '42',
464
+ baz: '43',
465
+ ignore: 'ignored'
466
+ }
467
+ }
468
+ const stringify = build(schema, { schema: { externalSchema } })
469
+ t.equal(stringify(data), '{"a":{"base":"a","bar":"42"},"b":{"base":"b","baz":"43"}}')
470
+ })
@@ -126,17 +126,18 @@ test('object with multiple required field not in properties schema', (t) => {
126
126
  stringify({})
127
127
  t.fail()
128
128
  } catch (e) {
129
- t.equal(e.message, '"num" is required!')
129
+ t.equal(e.message, '"key1" is required!')
130
130
  t.pass()
131
131
  }
132
132
 
133
133
  try {
134
134
  stringify({
135
- num: 42
135
+ key1: 42,
136
+ key2: 42
136
137
  })
137
138
  t.fail()
138
139
  } catch (e) {
139
- t.equal(e.message, '"key1" is required!')
140
+ t.equal(e.message, '"num" is required!')
140
141
  t.pass()
141
142
  }
142
143
 
@@ -127,7 +127,7 @@ test('not fail on null sub-object declared nullable', (t) => {
127
127
  t.equal('{"product":null}', stringify(object))
128
128
  })
129
129
 
130
- test('throw an error on non nullable null sub-object', (t) => {
130
+ test('on non nullable null sub-object it should coerce to {}', (t) => {
131
131
  t.plan(1)
132
132
 
133
133
  const stringify = build({
@@ -148,10 +148,12 @@ test('throw an error on non nullable null sub-object', (t) => {
148
148
  const object = {
149
149
  product: null
150
150
  }
151
- t.throws(() => { stringify(object) })
151
+
152
+ const result = stringify(object)
153
+ t.equal(result, JSON.stringify({ product: {} }))
152
154
  })
153
155
 
154
- test('throw an error on non nullable null object', (t) => {
156
+ test('on non nullable null object it should coerce to {}', (t) => {
155
157
  t.plan(1)
156
158
 
157
159
  const stringify = build({
@@ -170,5 +172,32 @@ test('throw an error on non nullable null object', (t) => {
170
172
  }
171
173
  }
172
174
  })
173
- t.throws(() => { stringify(null) })
175
+
176
+ const result = stringify(null)
177
+ t.equal(result, '{}')
178
+ })
179
+
180
+ test('on non-nullable null object it should skip rendering, skipping required fields checks', (t) => {
181
+ t.plan(1)
182
+
183
+ const stringify = build({
184
+ title: 'simple object',
185
+ nullable: false,
186
+ type: 'object',
187
+ properties: {
188
+ product: {
189
+ nullable: false,
190
+ type: 'object',
191
+ properties: {
192
+ name: {
193
+ type: 'string'
194
+ }
195
+ }
196
+ }
197
+ },
198
+ required: ['product']
199
+ })
200
+
201
+ const result = stringify(null)
202
+ t.equal(result, '{}')
174
203
  })
@@ -467,7 +467,7 @@ test('class instance that is simultaneously a string and a json', (t) => {
467
467
  t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
468
468
  })
469
469
 
470
- test('should throw an error when type is array and object is null', (t) => {
470
+ test('should not throw an error when type is array and object is null, it should instead coerce to []', (t) => {
471
471
  t.plan(1)
472
472
  const schema = {
473
473
  type: 'object',
@@ -482,7 +482,8 @@ test('should throw an error when type is array and object is null', (t) => {
482
482
  }
483
483
 
484
484
  const stringify = build(schema)
485
- t.throws(() => stringify({ arr: null }), new TypeError('The value of \'#/properties/arr\' does not match schema definition.'))
485
+ const result = stringify({ arr: null })
486
+ t.equal(result, JSON.stringify({ arr: [] }))
486
487
  })
487
488
 
488
489
  test('should throw an error when type is array and object is not an array', (t) => {