fast-json-stringify 5.0.4 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ajv.js CHANGED
@@ -7,29 +7,22 @@ const ajvFormats = require('ajv-formats')
7
7
  module.exports = buildAjv
8
8
 
9
9
  function buildAjv (options) {
10
- const ajvInstance = new Ajv({ ...options, strictSchema: false, validateSchema: false, uriResolver: fastUri })
11
- ajvFormats(ajvInstance)
10
+ const ajvInstance = new Ajv({
11
+ ...options,
12
+ strictSchema: false,
13
+ validateSchema: false,
14
+ allowUnionTypes: true,
15
+ uriResolver: fastUri
16
+ })
12
17
 
13
- const validateDateTimeFormat = ajvFormats.get('date-time').validate
14
- const validateDateFormat = ajvFormats.get('date').validate
15
- const validateTimeFormat = ajvFormats.get('time').validate
18
+ ajvFormats(ajvInstance)
16
19
 
17
20
  ajvInstance.addKeyword({
18
- keyword: 'fjs_date_type',
19
- validate: (schema, date) => {
20
- if (date instanceof Date) {
21
- return true
22
- }
23
- if (schema === 'date-time') {
24
- return validateDateTimeFormat(date)
25
- }
26
- if (schema === 'date') {
27
- return validateDateFormat(date)
28
- }
29
- if (schema === 'time') {
30
- return validateTimeFormat(date)
31
- }
32
- return false
21
+ keyword: 'fjs_type',
22
+ type: 'object',
23
+ errors: false,
24
+ validate: (type, date) => {
25
+ return date instanceof Date
33
26
  }
34
27
  })
35
28
 
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  /* eslint no-prototype-builtins: 0 */
4
4
 
5
- const merge = require('deepmerge')
5
+ const merge = require('@fastify/deepmerge')()
6
6
  const clone = require('rfdc')({ proto: true })
7
7
  const fjsCloned = Symbol('fast-json-stringify.cloned')
8
8
  const { randomUUID } = require('crypto')
@@ -55,7 +55,7 @@ function resolveRef (location, ref) {
55
55
  }
56
56
 
57
57
  const schemaId = ref.slice(0, hashIndex) || location.schemaId
58
- const jsonPointer = ref.slice(hashIndex)
58
+ const jsonPointer = ref.slice(hashIndex) || '#'
59
59
 
60
60
  const schemaRef = schemaId + jsonPointer
61
61
 
@@ -243,15 +243,6 @@ function inferTypeByKeyword (schema) {
243
243
  return schema.type
244
244
  }
245
245
 
246
- function getStringSerializer (format, nullable) {
247
- switch (format) {
248
- case 'date-time': return nullable ? 'serializer.asDatetimeNullable.bind(serializer)' : 'serializer.asDatetime.bind(serializer)'
249
- case 'date': return nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
250
- case 'time': return nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
251
- default: return nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
252
- }
253
- }
254
-
255
246
  function addPatternProperties (location) {
256
247
  const schema = location.schema
257
248
  const pp = schema.patternProperties
@@ -484,6 +475,16 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
484
475
  mergedSchema.anyOf.push(...allOfSchema.anyOf)
485
476
  }
486
477
 
478
+ if (allOfSchema.fjs_type !== undefined) {
479
+ if (
480
+ mergedSchema.fjs_type !== undefined &&
481
+ mergedSchema.fjs_type !== allOfSchema.fjs_type
482
+ ) {
483
+ throw new Error('allOf schemas have different fjs_type values')
484
+ }
485
+ mergedSchema.fjs_type = allOfSchema.fjs_type
486
+ }
487
+
487
488
  if (allOfSchema.allOf !== undefined) {
488
489
  mergeAllOfSchema(location, allOfSchema, mergedSchema)
489
490
  }
@@ -493,6 +494,7 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
493
494
  mergedSchema.$id = `merged_${randomUUID()}`
494
495
  ajvInstance.addSchema(mergedSchema)
495
496
  location.schemaId = mergedSchema.$id
497
+ location.jsonPointer = '#'
496
498
  }
497
499
 
498
500
  function buildInnerObject (location) {
@@ -770,6 +772,10 @@ function generateFuncName () {
770
772
  function buildValue (location, input) {
771
773
  let schema = location.schema
772
774
 
775
+ if (typeof schema === 'boolean') {
776
+ return `json += JSON.stringify(${input})`
777
+ }
778
+
773
779
  if (schema.$ref) {
774
780
  location = resolveRef(location, schema.$ref)
775
781
  schema = location.schema
@@ -789,20 +795,22 @@ function buildValue (location, input) {
789
795
  location.schema = mergedSchema
790
796
  }
791
797
 
792
- const type = schema.type
798
+ let type = schema.type
793
799
  const nullable = schema.nullable === true
794
800
 
795
801
  let code = ''
796
802
  let funcName
797
803
 
804
+ if (schema.fjs_type === 'string' && schema.format === undefined && Array.isArray(schema.type) && schema.type.length === 2) {
805
+ type = 'string'
806
+ }
807
+
798
808
  switch (type) {
799
809
  case 'null':
800
- code += `
801
- json += serializer.asNull()
802
- `
810
+ code += 'json += serializer.asNull()'
803
811
  break
804
812
  case 'string': {
805
- funcName = getStringSerializer(schema.format, nullable)
813
+ funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
806
814
  code += `json += ${funcName}(${input})`
807
815
  break
808
816
  }
@@ -819,7 +827,15 @@ function buildValue (location, input) {
819
827
  code += `json += ${funcName}(${input})`
820
828
  break
821
829
  case 'object':
822
- funcName = buildObject(location)
830
+ if (schema.format === 'date-time') {
831
+ funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)'
832
+ } else if (schema.format === 'date') {
833
+ funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
834
+ } else if (schema.format === 'time') {
835
+ funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
836
+ } else {
837
+ funcName = buildObject(location)
838
+ }
823
839
  code += `json += ${funcName}(${input})`
824
840
  break
825
841
  case 'array':
@@ -827,11 +843,7 @@ function buildValue (location, input) {
827
843
  code += `json += ${funcName}(${input})`
828
844
  break
829
845
  case undefined:
830
- if (schema.fjs_date_type) {
831
- funcName = getStringSerializer(schema.fjs_date_type, nullable)
832
- code += `json += ${funcName}(${input})`
833
- break
834
- } else if (schema.anyOf || schema.oneOf) {
846
+ if (schema.anyOf || schema.oneOf) {
835
847
  // beware: dereferenceOfRefs has side effects and changes schema.anyOf
836
848
  const type = schema.anyOf ? 'anyOf' : 'oneOf'
837
849
  const anyOfLocation = mergeLocation(location, type)
@@ -889,7 +901,7 @@ function buildValue (location, input) {
889
901
  switch (type) {
890
902
  case 'string': {
891
903
  code += `
892
- ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof Date || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
904
+ ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
893
905
  ${nestedResult}
894
906
  `
895
907
  break
@@ -908,6 +920,20 @@ function buildValue (location, input) {
908
920
  `
909
921
  break
910
922
  }
923
+ case 'object': {
924
+ if (schema.fjs_type) {
925
+ code += `
926
+ ${statement}(${input} instanceof Date || ${input} === null)
927
+ ${nestedResult}
928
+ `
929
+ } else {
930
+ code += `
931
+ ${statement}(typeof ${input} === "object" || ${input} === null)
932
+ ${nestedResult}
933
+ `
934
+ }
935
+ break
936
+ }
911
937
  default: {
912
938
  code += `
913
939
  ${statement}(typeof ${input} === "${type}" || ${input} === null)
@@ -935,15 +961,21 @@ function buildValue (location, input) {
935
961
  }
936
962
 
937
963
  // Ajv does not support js date format. In order to properly validate objects containing a date,
938
- // it needs to replace all occurrences of the string date format with a custom keyword fjs_date_type.
964
+ // it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
939
965
  // (see https://github.com/fastify/fast-json-stringify/pull/441)
940
966
  function extendDateTimeType (schema) {
941
967
  if (schema === null) return
942
968
 
943
- if (schema.type === 'string' && ['date-time', 'date', 'time'].includes(schema.format)) {
944
- schema.fjs_date_type = schema.format
945
- delete schema.type
946
- delete schema.format
969
+ if (schema.type === 'string') {
970
+ schema.fjs_type = 'string'
971
+ schema.type = ['string', 'object']
972
+ } else if (
973
+ Array.isArray(schema.type) &&
974
+ schema.type.includes('string') &&
975
+ !schema.type.includes('object')
976
+ ) {
977
+ schema.fjs_type = 'string'
978
+ schema.type.push('object')
947
979
  }
948
980
  for (const property in schema) {
949
981
  if (typeof schema[property] === 'object') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.0.4",
3
+ "version": "5.1.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "homepage": "https://github.com/fastify/fast-json-stringify#readme",
35
35
  "devDependencies": {
36
- "@sinclair/typebox": "^0.23.3",
36
+ "@sinclair/typebox": "^0.24.9",
37
37
  "benchmark": "^2.1.4",
38
38
  "cli-select": "^1.1.2",
39
39
  "compile-json-stringify": "^0.1.2",
@@ -49,9 +49,9 @@
49
49
  "webpack": "^5.40.0"
50
50
  },
51
51
  "dependencies": {
52
+ "@fastify/deepmerge": "^1.0.0",
52
53
  "ajv": "^8.10.0",
53
54
  "ajv-formats": "^2.1.1",
54
- "deepmerge": "^4.2.2",
55
55
  "fast-uri": "^2.1.0",
56
56
  "rfdc": "^1.2.0"
57
57
  },
package/serializer.js CHANGED
@@ -69,24 +69,24 @@ module.exports = class Serializer {
69
69
  return bool === null ? 'null' : this.asBoolean(bool)
70
70
  }
71
71
 
72
- asDatetime (date) {
73
- const quotes = '"'
72
+ asDateTime (date) {
73
+ if (date === null) return '""'
74
74
  if (date instanceof Date) {
75
- return quotes + date.toISOString() + quotes
75
+ return '"' + date.toISOString() + '"'
76
76
  }
77
- return this.asString(date)
77
+ throw new Error(`The value "${date}" cannot be converted to a date-time.`)
78
78
  }
79
79
 
80
- asDatetimeNullable (date) {
81
- return date === null ? 'null' : this.asDatetime(date)
80
+ asDateTimeNullable (date) {
81
+ return date === null ? 'null' : this.asDateTime(date)
82
82
  }
83
83
 
84
84
  asDate (date) {
85
- const quotes = '"'
85
+ if (date === null) return '""'
86
86
  if (date instanceof Date) {
87
- return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + quotes
87
+ return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"'
88
88
  }
89
- return this.asString(date)
89
+ throw new Error(`The value "${date}" cannot be converted to a date.`)
90
90
  }
91
91
 
92
92
  asDateNullable (date) {
@@ -94,11 +94,11 @@ module.exports = class Serializer {
94
94
  }
95
95
 
96
96
  asTime (date) {
97
- const quotes = '"'
97
+ if (date === null) return '""'
98
98
  if (date instanceof Date) {
99
- return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + quotes
99
+ return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"'
100
100
  }
101
- return this.asString(date)
101
+ throw new Error(`The value "${date}" cannot be converted to a time.`)
102
102
  }
103
103
 
104
104
  asTimeNullable (date) {
@@ -359,3 +359,16 @@ test('render a single quote as JSON', (t) => {
359
359
  t.equal(output, JSON.stringify(toStringify))
360
360
  t.ok(validate(JSON.parse(output)), 'valid schema')
361
361
  })
362
+
363
+ test('returns JSON.stringify if schema type is boolean', t => {
364
+ t.plan(1)
365
+
366
+ const schema = {
367
+ type: 'array',
368
+ items: true
369
+ }
370
+
371
+ const array = [1, true, 'test']
372
+ const stringify = build(schema)
373
+ t.equal(stringify(array), JSON.stringify(array))
374
+ })
package/test/date.test.js CHANGED
@@ -308,7 +308,7 @@ test('serializing null value', t => {
308
308
  })
309
309
 
310
310
  t.test('type::array', t => {
311
- t.plan(3)
311
+ t.plan(6)
312
312
 
313
313
  t.test('format::date-time', t => {
314
314
  t.plan(2)
@@ -348,13 +348,13 @@ test('serializing null value', t => {
348
348
  t.notOk(validate(JSON.parse(output)), 'an empty string is not a date format')
349
349
  })
350
350
 
351
- t.test('format::time', t => {
351
+ t.test('format::date', t => {
352
352
  t.plan(2)
353
353
 
354
354
  const prop = {
355
355
  updatedAt: {
356
356
  type: ['string'],
357
- format: 'time'
357
+ format: 'date'
358
358
  }
359
359
  }
360
360
 
@@ -364,7 +364,66 @@ test('serializing null value', t => {
364
364
  } = serialize(createSchema(prop), input)
365
365
 
366
366
  t.equal(output, '{"updatedAt":""}')
367
- t.notOk(validate(JSON.parse(output)), 'an empty string is not a time format')
367
+ t.notOk(validate(JSON.parse(output)), 'an empty string is not a date format')
368
+ })
369
+
370
+ t.test('format::time, Date object', t => {
371
+ t.plan(1)
372
+
373
+ const schema = {
374
+ oneOf: [
375
+ {
376
+ type: 'object',
377
+ properties: {
378
+ updatedAt: {
379
+ type: ['string', 'number'],
380
+ format: 'time'
381
+ }
382
+ }
383
+ }
384
+ ]
385
+ }
386
+
387
+ const date = new Date()
388
+ const input = { updatedAt: date }
389
+ const { output } = serialize(schema, input)
390
+
391
+ t.equal(output, JSON.stringify({ updatedAt: DateTime.fromJSDate(date).toFormat('HH:mm:ss') }))
392
+ })
393
+
394
+ t.test('format::time, Date object', t => {
395
+ t.plan(1)
396
+
397
+ const schema = {
398
+ oneOf: [
399
+ {
400
+ type: ['string', 'number'],
401
+ format: 'time'
402
+ }
403
+ ]
404
+ }
405
+
406
+ const date = new Date()
407
+ const { output } = serialize(schema, date)
408
+
409
+ t.equal(output, `"${DateTime.fromJSDate(date).toFormat('HH:mm:ss')}"`)
410
+ })
411
+
412
+ t.test('format::time, Date object', t => {
413
+ t.plan(1)
414
+
415
+ const schema = {
416
+ oneOf: [
417
+ {
418
+ type: ['string', 'number'],
419
+ format: 'time'
420
+ }
421
+ ]
422
+ }
423
+
424
+ const { output } = serialize(schema, 42)
425
+
426
+ t.equal(output, JSON.stringify(42))
368
427
  })
369
428
  })
370
429
 
@@ -429,3 +488,40 @@ test('serializing null value', t => {
429
488
  })
430
489
  })
431
490
  })
491
+
492
+ test('Validate Date object as string type', (t) => {
493
+ t.plan(1)
494
+
495
+ const schema = {
496
+ oneOf: [
497
+ { type: 'string' }
498
+ ]
499
+ }
500
+ const toStringify = new Date()
501
+
502
+ const stringify = build(schema)
503
+ const output = stringify(toStringify)
504
+
505
+ t.equal(output, JSON.stringify(toStringify))
506
+ })
507
+
508
+ test('nullable date', (t) => {
509
+ t.plan(1)
510
+
511
+ const schema = {
512
+ anyOf: [
513
+ {
514
+ format: 'date',
515
+ type: 'string',
516
+ nullable: true
517
+ }
518
+ ]
519
+ }
520
+
521
+ const stringify = build(schema)
522
+
523
+ const data = new Date()
524
+ const result = stringify(data)
525
+
526
+ t.same(result, `"${DateTime.fromJSDate(data).toISODate()}"`)
527
+ })
package/test/ref.test.js CHANGED
@@ -1891,3 +1891,65 @@ test('input schema is not mutated', (t) => {
1891
1891
  t.equal(output, '{"obj":"test"}')
1892
1892
  t.same(schema, clonedSchema)
1893
1893
  })
1894
+
1895
+ test('anyOf inside allOf', (t) => {
1896
+ t.plan(1)
1897
+
1898
+ const schema = {
1899
+ anyOf: [
1900
+ {
1901
+ type: 'object',
1902
+ allOf: [
1903
+ {
1904
+ properties: {
1905
+ a: {
1906
+ anyOf: [
1907
+ { const: 'A1' },
1908
+ { const: 'A2' }
1909
+ ]
1910
+ }
1911
+ }
1912
+ },
1913
+ {
1914
+ properties: {
1915
+ b: { const: 'B' }
1916
+ }
1917
+ }
1918
+ ]
1919
+ }
1920
+ ]
1921
+ }
1922
+
1923
+ const object = { a: 'A1', b: 'B' }
1924
+ const stringify = build(schema)
1925
+ const output = stringify(object)
1926
+
1927
+ t.equal(output, JSON.stringify(object))
1928
+ })
1929
+
1930
+ test('should resolve absolute $refs', (t) => {
1931
+ t.plan(1)
1932
+
1933
+ const externalSchema = {
1934
+ FooSchema: {
1935
+ $id: 'FooSchema',
1936
+ type: 'object',
1937
+ properties: {
1938
+ type: {
1939
+ anyOf: [
1940
+ { type: 'string', const: 'bar' },
1941
+ { type: 'string', const: 'baz' }
1942
+ ]
1943
+ }
1944
+ }
1945
+ }
1946
+ }
1947
+
1948
+ const schema = { $ref: 'FooSchema' }
1949
+
1950
+ const object = { type: 'bar' }
1951
+ const stringify = build(schema, { schema: externalSchema })
1952
+ const output = stringify(object)
1953
+
1954
+ t.equal(output, JSON.stringify(object))
1955
+ })