fast-json-stringify 5.10.0 → 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/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
451
 
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
-
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) {
@@ -582,7 +499,7 @@ function buildArray (context, location) {
582
499
  itemsLocation.schema = itemsLocation.schema || {}
583
500
 
584
501
  if (itemsLocation.schema.$ref) {
585
- itemsLocation = resolveRef(context, itemsLocation, itemsLocation.schema.$ref)
502
+ itemsLocation = resolveRef(context, itemsLocation)
586
503
  }
587
504
 
588
505
  const itemsSchema = itemsLocation.schema
@@ -860,81 +777,193 @@ function buildConstSerializer (location, input) {
860
777
  return code
861
778
  }
862
779
 
863
- function buildValue (context, location, input) {
864
- let schema = location.schema
780
+ function buildAllOf (context, location, input) {
781
+ const schema = location.schema
865
782
 
866
- if (typeof schema === 'boolean') {
867
- 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)
868
787
  }
869
788
 
870
- if (schema.$ref) {
871
- location = resolveRef(context, location, schema.$ref)
872
- 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))
873
804
  }
874
805
 
875
- if (schema.type === undefined) {
876
- const inferredType = inferTypeByKeyword(schema)
877
- if (inferredType) {
878
- 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
+ ])
879
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
+ `
880
851
  }
881
852
 
882
- if (schema.if && schema.then) {
883
- return addIfThenElse(context, location, input)
853
+ let schemaRef = location.getSchemaRef()
854
+ if (schemaRef.startsWith(context.rootSchemaId)) {
855
+ schemaRef = schemaRef.replace(context.rootSchemaId, '')
884
856
  }
885
857
 
886
- if (schema.allOf) {
887
- mergeAllOfSchema(context, location, schema, clone(schema))
888
- 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
+ ])
889
897
  }
890
898
 
891
- 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
+ }
892
908
 
893
- 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
894
917
 
895
- if ((type === undefined || type === 'object') && (schema.anyOf || schema.oneOf)) {
896
- context.validatorSchemasIds.add(location.getSchemaId())
918
+ elseMergedLocation = mergeLocations(context, elseMergedSchemaId, [
919
+ rootLocation,
920
+ elseLocation
921
+ ])
922
+ }
897
923
 
898
- if (schema.type === 'object') {
899
- context.wrapObjects = false
900
- const funcName = buildObject(context, location)
901
- code += `
902
- json += '{'
903
- json += ${funcName}(${input})
904
- json += ','
905
- `
924
+ return `
925
+ if (validator.validate("${ifSchemaRef}", ${input})) {
926
+ ${buildValue(context, thenMergedLocation, input)}
927
+ } else {
928
+ ${buildValue(context, elseMergedLocation, input)}
906
929
  }
930
+ `
931
+ }
907
932
 
908
- const type = schema.anyOf ? 'anyOf' : 'oneOf'
909
- const anyOfLocation = location.getPropertyLocation(type)
933
+ function buildValue (context, location, input) {
934
+ let schema = location.schema
910
935
 
911
- for (let index = 0; index < location.schema[type].length; index++) {
912
- const optionLocation = anyOfLocation.getPropertyLocation(index)
913
- const schemaRef = optionLocation.getSchemaRef()
914
- const nestedResult = buildValue(context, optionLocation, input)
915
- code += `
916
- ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
917
- ${nestedResult}
918
- `
919
- }
936
+ if (typeof schema === 'boolean') {
937
+ return `json += JSON.stringify(${input})`
938
+ }
920
939
 
921
- let schemaRef = location.getSchemaRef()
922
- if (schemaRef.startsWith(context.rootSchemaId)) {
923
- schemaRef = schemaRef.replace(context.rootSchemaId, '')
924
- }
940
+ if (schema.$ref) {
941
+ location = resolveRef(context, location)
942
+ schema = location.schema
943
+ }
925
944
 
926
- code += `
927
- else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
928
- `
929
- if (schema.type === 'object') {
930
- code += `
931
- json += '}'
932
- `
933
- 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
934
961
  }
935
- return code
936
962
  }
937
963
 
964
+ let code = ''
965
+
966
+ const type = schema.type
938
967
  const nullable = schema.nullable === true
939
968
  if (nullable) {
940
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.10.0",
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