fast-json-stringify 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,15 +11,15 @@ jobs:
11
11
  permissions:
12
12
  contents: read
13
13
  outputs:
14
- PR-BENCH-12: ${{ steps.benchmark-pr.outputs.BENCH_RESULT12 }}
15
14
  PR-BENCH-14: ${{ steps.benchmark-pr.outputs.BENCH_RESULT14 }}
16
15
  PR-BENCH-16: ${{ steps.benchmark-pr.outputs.BENCH_RESULT16 }}
17
- MAIN-BENCH-12: ${{ steps.benchmark-main.outputs.BENCH_RESULT12 }}
16
+ PR-BENCH-18: ${{ steps.benchmark-pr.outputs.BENCH_RESULT18 }}
18
17
  MAIN-BENCH-14: ${{ steps.benchmark-main.outputs.BENCH_RESULT14 }}
19
18
  MAIN-BENCH-16: ${{ steps.benchmark-main.outputs.BENCH_RESULT16 }}
19
+ MAIN-BENCH-18: ${{ steps.benchmark-main.outputs.BENCH_RESULT18 }}
20
20
  strategy:
21
21
  matrix:
22
- node-version: [12, 14, 16]
22
+ node-version: [14, 16, 18]
23
23
  steps:
24
24
  - uses: actions/checkout@v3
25
25
  with:
@@ -75,38 +75,38 @@ jobs:
75
75
  with:
76
76
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77
77
  message: |
78
- **Node**: 12
78
+ **Node**: 14
79
79
  **PR**:
80
80
  ```
81
- ${{ needs.benchmark.outputs.PR-BENCH-12 }}
81
+ ${{ needs.benchmark.outputs.PR-BENCH-14 }}
82
82
  ```
83
83
  **MAIN**:
84
84
  ```
85
- ${{ needs.benchmark.outputs.MAIN-BENCH-12 }}
85
+ ${{ needs.benchmark.outputs.MAIN-BENCH-14 }}
86
86
  ```
87
87
 
88
88
  ---
89
89
 
90
- **Node**: 14
90
+ **Node**: 16
91
91
  **PR**:
92
92
  ```
93
- ${{ needs.benchmark.outputs.PR-BENCH-14 }}
93
+ ${{ needs.benchmark.outputs.PR-BENCH-16 }}
94
94
  ```
95
95
  **MAIN**:
96
96
  ```
97
- ${{ needs.benchmark.outputs.MAIN-BENCH-14 }}
97
+ ${{ needs.benchmark.outputs.MAIN-BENCH-16 }}
98
98
  ```
99
99
 
100
100
  ---
101
101
 
102
- **Node**: 16
102
+ **Node**: 18
103
103
  **PR**:
104
104
  ```
105
- ${{ needs.benchmark.outputs.PR-BENCH-16 }}
105
+ ${{ needs.benchmark.outputs.PR-BENCH-18 }}
106
106
  ```
107
107
  **MAIN**:
108
108
  ```
109
- ${{ needs.benchmark.outputs.MAIN-BENCH-16 }}
109
+ ${{ needs.benchmark.outputs.MAIN-BENCH-18 }}
110
110
  ```
111
111
 
112
112
  - uses: actions-ecosystem/action-remove-labels@v1
@@ -13,3 +13,5 @@ on:
13
13
  jobs:
14
14
  test:
15
15
  uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
16
+ with:
17
+ license-check: true
@@ -213,6 +213,60 @@ const benchmarks = [
213
213
  n5: 42,
214
214
  b5: true
215
215
  }
216
+ },
217
+ {
218
+ name: 'object with const string property',
219
+ schema: {
220
+ type: 'object',
221
+ properties: {
222
+ a: { const: 'const string' }
223
+ }
224
+ },
225
+ input: { a: 'const string' }
226
+ },
227
+ {
228
+ name: 'object with const number property',
229
+ schema: {
230
+ type: 'object',
231
+ properties: {
232
+ a: { const: 1 }
233
+ }
234
+ },
235
+ input: { a: 1 }
236
+ },
237
+ {
238
+ name: 'object with const bool property',
239
+ schema: {
240
+ type: 'object',
241
+ properties: {
242
+ a: { const: true }
243
+ }
244
+ },
245
+ input: { a: true }
246
+ },
247
+ {
248
+ name: 'object with const object property',
249
+ schema: {
250
+ type: 'object',
251
+ properties: {
252
+ foo: { const: { bar: 'baz' } }
253
+ }
254
+ },
255
+ input: {
256
+ foo: { bar: 'baz' }
257
+ }
258
+ },
259
+ {
260
+ name: 'object with const null property',
261
+ schema: {
262
+ type: 'object',
263
+ properties: {
264
+ foo: { const: null }
265
+ }
266
+ },
267
+ input: {
268
+ foo: null
269
+ }
216
270
  }
217
271
  ]
218
272
 
package/index.js CHANGED
@@ -9,7 +9,8 @@ const { randomUUID } = require('crypto')
9
9
 
10
10
  const validate = require('./schema-validator')
11
11
  const Serializer = require('./serializer')
12
- const buildAjv = require('./ajv')
12
+ const Validator = require('./validator')
13
+ const RefResolver = require('./ref-resolver')
13
14
 
14
15
  let largeArraySize = 2e4
15
16
  let largeArrayMechanism = 'default'
@@ -57,20 +58,12 @@ function resolveRef (location, ref) {
57
58
  const schemaId = ref.slice(0, hashIndex) || location.schemaId
58
59
  const jsonPointer = ref.slice(hashIndex) || '#'
59
60
 
60
- const schemaRef = schemaId + jsonPointer
61
+ const schema = refResolver.getSchema(schemaId, jsonPointer)
61
62
 
62
- let ajvSchema
63
- try {
64
- ajvSchema = ajvInstance.getSchema(schemaRef)
65
- } catch (error) {
63
+ if (schema === undefined) {
66
64
  throw new Error(`Cannot find reference "${ref}"`)
67
65
  }
68
66
 
69
- if (ajvSchema === undefined) {
70
- throw new Error(`Cannot find reference "${ref}"`)
71
- }
72
-
73
- const schema = ajvSchema.schema
74
67
  if (schema.$ref !== undefined) {
75
68
  return resolveRef({ schema, schemaId, jsonPointer }, schema.$ref)
76
69
  }
@@ -82,41 +75,31 @@ const arrayItemsReferenceSerializersMap = new Map()
82
75
  const objectReferenceSerializersMap = new Map()
83
76
 
84
77
  let rootSchemaId = null
85
- let ajvInstance = null
78
+ let refResolver = null
79
+ let validator = null
86
80
  let contextFunctions = null
87
81
 
88
82
  function build (schema, options) {
89
- schema = clone(schema)
90
-
91
83
  arrayItemsReferenceSerializersMap.clear()
92
84
  objectReferenceSerializersMap.clear()
93
85
 
94
86
  contextFunctions = []
95
87
  options = options || {}
96
88
 
97
- ajvInstance = buildAjv(options.ajv)
89
+ refResolver = new RefResolver()
90
+ validator = new Validator(options.ajv)
91
+
98
92
  rootSchemaId = schema.$id || randomUUID()
99
93
 
100
94
  isValidSchema(schema)
101
- extendDateTimeType(schema)
102
- ajvInstance.addSchema(schema, rootSchemaId)
95
+ validator.addSchema(schema, rootSchemaId)
96
+ refResolver.addSchema(schema, rootSchemaId)
103
97
 
104
98
  if (options.schema) {
105
- const externalSchemas = clone(options.schema)
106
-
107
- for (const key of Object.keys(externalSchemas)) {
108
- const externalSchema = externalSchemas[key]
109
- isValidSchema(externalSchema, key)
110
- extendDateTimeType(externalSchema)
111
-
112
- let schemaKey = externalSchema.$id || key
113
- if (externalSchema.$id !== undefined && externalSchema.$id[0] === '#') {
114
- schemaKey = key + externalSchema.$id // relative URI
115
- }
116
-
117
- if (ajvInstance.getSchema(schemaKey) === undefined) {
118
- ajvInstance.addSchema(externalSchema, schemaKey)
119
- }
99
+ for (const key of Object.keys(options.schema)) {
100
+ isValidSchema(options.schema[key], key)
101
+ validator.addSchema(options.schema[key], key)
102
+ refResolver.addSchema(options.schema[key], key)
120
103
  }
121
104
  }
122
105
 
@@ -157,27 +140,28 @@ function build (schema, options) {
157
140
  return main
158
141
  `
159
142
 
160
- const dependenciesName = ['ajv', 'serializer', contextFunctionCode]
143
+ const dependenciesName = ['validator', 'serializer', contextFunctionCode]
161
144
 
162
145
  if (options.debugMode) {
163
146
  options.mode = 'debug'
164
147
  }
165
148
 
166
149
  if (options.mode === 'debug') {
167
- return { code: dependenciesName.join('\n'), ajv: ajvInstance }
150
+ return { code: dependenciesName.join('\n'), validator, ajv: validator.ajv }
168
151
  }
169
152
 
170
153
  if (options.mode === 'standalone') {
171
154
  // lazy load
172
155
  const buildStandaloneCode = require('./standalone')
173
- return buildStandaloneCode(options, ajvInstance, contextFunctionCode)
156
+ return buildStandaloneCode(options, validator, contextFunctionCode)
174
157
  }
175
158
 
176
159
  /* eslint no-new-func: "off" */
177
- const contextFunc = new Function('ajv', 'serializer', contextFunctionCode)
178
- const stringifyFunc = contextFunc(ajvInstance, serializer)
160
+ const contextFunc = new Function('validator', 'serializer', contextFunctionCode)
161
+ const stringifyFunc = contextFunc(validator, serializer)
179
162
 
180
- ajvInstance = null
163
+ refResolver = null
164
+ validator = null
181
165
  rootSchemaId = null
182
166
  contextFunctions = null
183
167
  arrayItemsReferenceSerializersMap.clear()
@@ -341,17 +325,16 @@ function buildCode (location) {
341
325
  const propertiesLocation = mergeLocation(location, 'properties')
342
326
  Object.keys(schema.properties || {}).forEach((key) => {
343
327
  let propertyLocation = mergeLocation(propertiesLocation, key)
344
- if (schema.properties[key].$ref) {
345
- propertyLocation = resolveRef(location, schema.properties[key].$ref)
346
- schema.properties[key] = propertyLocation.schema
328
+ if (propertyLocation.$ref) {
329
+ propertyLocation = resolveRef(location, propertyLocation.$ref)
347
330
  }
348
331
 
349
- // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
350
- // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
351
-
352
332
  const sanitized = JSON.stringify(key)
353
333
  const asString = JSON.stringify(sanitized)
354
334
 
335
+ // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
336
+ // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
337
+
355
338
  code += `
356
339
  if (obj[${sanitized}] !== undefined) {
357
340
  ${addComma}
@@ -360,7 +343,7 @@ function buildCode (location) {
360
343
 
361
344
  code += buildValue(propertyLocation, `obj[${JSON.stringify(key)}]`)
362
345
 
363
- const defaultValue = schema.properties[key].default
346
+ const defaultValue = propertyLocation.schema.default
364
347
  if (defaultValue !== undefined) {
365
348
  code += `
366
349
  } else {
@@ -475,16 +458,6 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
475
458
  mergedSchema.anyOf.push(...allOfSchema.anyOf)
476
459
  }
477
460
 
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
-
488
461
  if (allOfSchema.allOf !== undefined) {
489
462
  mergeAllOfSchema(location, allOfSchema, mergedSchema)
490
463
  }
@@ -492,7 +465,8 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
492
465
  delete mergedSchema.allOf
493
466
 
494
467
  mergedSchema.$id = `merged_${randomUUID()}`
495
- ajvInstance.addSchema(mergedSchema)
468
+ validator.addSchema(mergedSchema)
469
+ refResolver.addSchema(mergedSchema)
496
470
  location.schemaId = mergedSchema.$id
497
471
  location.jsonPointer = '#'
498
472
  }
@@ -521,7 +495,7 @@ function addIfThenElse (location) {
521
495
  const ifSchemaRef = ifLocation.schemaId + ifLocation.jsonPointer
522
496
 
523
497
  let code = `
524
- if (ajv.validate("${ifSchemaRef}", obj)) {
498
+ if (validator.validate("${ifSchemaRef}", obj)) {
525
499
  `
526
500
 
527
501
  const thenLocation = mergeLocation(location, 'then')
@@ -795,14 +769,21 @@ function buildValue (location, input) {
795
769
  location.schema = mergedSchema
796
770
  }
797
771
 
798
- let type = schema.type
799
- const nullable = schema.nullable === true
772
+ const type = schema.type
773
+ const nullable = schema.nullable === true || (Array.isArray(type) && type.includes('null'))
800
774
 
801
775
  let code = ''
802
776
  let funcName
803
777
 
804
- if (schema.fjs_type === 'string' && schema.format === undefined && Array.isArray(schema.type) && schema.type.length === 2) {
805
- type = 'string'
778
+ if ('const' in schema) {
779
+ if (nullable) {
780
+ code += `
781
+ json += ${input} === null ? 'null' : '${JSON.stringify(schema.const)}'
782
+ `
783
+ return code
784
+ }
785
+ code += `json += '${JSON.stringify(schema.const)}'`
786
+ return code
806
787
  }
807
788
 
808
789
  switch (type) {
@@ -810,7 +791,15 @@ function buildValue (location, input) {
810
791
  code += 'json += serializer.asNull()'
811
792
  break
812
793
  case 'string': {
813
- funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
794
+ if (schema.format === 'date-time') {
795
+ funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)'
796
+ } else if (schema.format === 'date') {
797
+ funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
798
+ } else if (schema.format === 'time') {
799
+ funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
800
+ } else {
801
+ funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
802
+ }
814
803
  code += `json += ${funcName}(${input})`
815
804
  break
816
805
  }
@@ -827,15 +816,7 @@ function buildValue (location, input) {
827
816
  code += `json += ${funcName}(${input})`
828
817
  break
829
818
  case 'object':
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
- }
819
+ funcName = buildObject(location)
839
820
  code += `json += ${funcName}(${input})`
840
821
  break
841
822
  case 'array':
@@ -853,7 +834,7 @@ function buildValue (location, input) {
853
834
  const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer
854
835
  const nestedResult = buildValue(optionLocation, input)
855
836
  code += `
856
- ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaRef}", ${input}))
837
+ ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
857
838
  ${nestedResult}
858
839
  `
859
840
  }
@@ -865,19 +846,10 @@ function buildValue (location, input) {
865
846
  code += `
866
847
  json += JSON.stringify(${input})
867
848
  `
868
- } else if ('const' in schema) {
869
- code += `
870
- if(ajv.validate(${JSON.stringify(schema)}, ${input}))
871
- json += '${JSON.stringify(schema.const)}'
872
- else
873
- throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`)
874
- `
875
- } else if (schema.type === undefined) {
849
+ } else {
876
850
  code += `
877
851
  json += JSON.stringify(${input})
878
852
  `
879
- } else {
880
- throw new Error(`${schema.type} unsupported`)
881
853
  }
882
854
  break
883
855
  default:
@@ -901,7 +873,18 @@ function buildValue (location, input) {
901
873
  switch (type) {
902
874
  case 'string': {
903
875
  code += `
904
- ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
876
+ ${statement}(
877
+ typeof ${input} === "string" ||
878
+ ${input} === null ||
879
+ ${input} instanceof Date ||
880
+ ${input} instanceof RegExp ||
881
+ (
882
+ typeof ${input} === "object" &&
883
+ typeof ${input}.toString === "function" &&
884
+ ${input}.toString !== Object.prototype.toString &&
885
+ !(${input} instanceof Date)
886
+ )
887
+ )
905
888
  ${nestedResult}
906
889
  `
907
890
  break
@@ -921,17 +904,10 @@ function buildValue (location, input) {
921
904
  break
922
905
  }
923
906
  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
- }
907
+ code += `
908
+ ${statement}(typeof ${input} === "object" || ${input} === null)
909
+ ${nestedResult}
910
+ `
935
911
  break
936
912
  }
937
913
  default: {
@@ -960,34 +936,10 @@ function buildValue (location, input) {
960
936
  return code
961
937
  }
962
938
 
963
- // Ajv does not support js date format. In order to properly validate objects containing a date,
964
- // it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
965
- // (see https://github.com/fastify/fast-json-stringify/pull/441)
966
- function extendDateTimeType (schema) {
967
- if (schema === null) return
968
-
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')
979
- }
980
- for (const property in schema) {
981
- if (typeof schema[property] === 'object') {
982
- extendDateTimeType(schema[property])
983
- }
984
- }
985
- }
986
-
987
939
  function isEmpty (schema) {
988
940
  // eslint-disable-next-line
989
941
  for (var key in schema) {
990
- if (schema.hasOwnProperty(key) && schema[key] !== undefined) {
942
+ if (Object.prototype.hasOwnProperty.call(schema, key) && schema[key] !== undefined) {
991
943
  return false
992
944
  }
993
945
  }
@@ -998,9 +950,9 @@ module.exports = build
998
950
 
999
951
  module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
1000
952
 
1001
- module.exports.restore = function ({ code, ajv }) {
953
+ module.exports.restore = function ({ code, validator }) {
1002
954
  const serializer = new Serializer()
1003
955
  // eslint-disable-next-line
1004
- return (Function.apply(null, ['ajv', 'serializer', code])
1005
- .apply(null, [ajv, serializer]))
956
+ return (Function.apply(null, ['validator', 'serializer', code])
957
+ .apply(null, [validator, serializer]))
1006
958
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -33,13 +33,13 @@
33
33
  },
34
34
  "homepage": "https://github.com/fastify/fast-json-stringify#readme",
35
35
  "devDependencies": {
36
+ "@fastify/pre-commit": "^2.0.2",
36
37
  "@sinclair/typebox": "^0.24.9",
37
38
  "benchmark": "^2.1.4",
38
39
  "cli-select": "^1.1.2",
39
40
  "compile-json-stringify": "^0.1.2",
40
41
  "is-my-json-valid": "^2.20.0",
41
- "luxon": "^2.4.0",
42
- "pre-commit": "^1.2.2",
42
+ "luxon": "^3.0.1",
43
43
  "proxyquire": "^2.1.3",
44
44
  "semver": "^7.1.0",
45
45
  "simple-git": "^3.7.1",
@@ -52,6 +52,7 @@
52
52
  "@fastify/deepmerge": "^1.0.0",
53
53
  "ajv": "^8.10.0",
54
54
  "ajv-formats": "^2.1.1",
55
+ "fast-deep-equal": "^3.1.3",
55
56
  "fast-uri": "^2.1.0",
56
57
  "rfdc": "^1.2.0"
57
58
  },
@@ -0,0 +1,84 @@
1
+ 'use strict'
2
+
3
+ const deepEqual = require('fast-deep-equal')
4
+
5
+ class RefResolver {
6
+ constructor () {
7
+ this.schemas = {}
8
+ }
9
+
10
+ addSchema (schema, schemaId) {
11
+ if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {
12
+ schemaId = schema.$id
13
+ }
14
+ if (this.getSchema(schemaId) === undefined) {
15
+ this.insertSchemaBySchemaId(schema, schemaId)
16
+ this.insertSchemaSubschemas(schema, schemaId)
17
+ }
18
+ }
19
+
20
+ getSchema (schemaId, jsonPointer = '#') {
21
+ const schema = this.schemas[schemaId]
22
+ if (schema === undefined) {
23
+ return undefined
24
+ }
25
+ if (schema.anchors[jsonPointer] !== undefined) {
26
+ return schema.anchors[jsonPointer]
27
+ }
28
+ return getDataByJSONPointer(schema.schema, jsonPointer)
29
+ }
30
+
31
+ insertSchemaBySchemaId (schema, schemaId) {
32
+ if (
33
+ this.schemas[schemaId] !== undefined &&
34
+ !deepEqual(schema, this.schemas[schemaId].schema)
35
+ ) {
36
+ throw new Error(`There is already another schema with id ${schemaId}`)
37
+ }
38
+ this.schemas[schemaId] = { schema, anchors: {} }
39
+ }
40
+
41
+ insertSchemaByAnchor (schema, schemaId, anchor) {
42
+ const { anchors } = this.schemas[schemaId]
43
+ if (
44
+ anchors[anchor] !== undefined &&
45
+ !deepEqual(schema, anchors[anchor])
46
+ ) {
47
+ throw new Error(`There is already another schema with id ${schemaId}#${anchor}`)
48
+ }
49
+ anchors[anchor] = schema
50
+ }
51
+
52
+ insertSchemaSubschemas (schema, rootSchemaId) {
53
+ const schemaId = schema.$id
54
+ if (schemaId !== undefined && typeof schemaId === 'string') {
55
+ if (schemaId.charAt(0) === '#') {
56
+ this.insertSchemaByAnchor(schema, rootSchemaId, schemaId)
57
+ } else {
58
+ this.insertSchemaBySchemaId(schema, schemaId)
59
+ rootSchemaId = schemaId
60
+ }
61
+ }
62
+
63
+ for (const key in schema) {
64
+ if (typeof schema[key] === 'object' && schema[key] !== null) {
65
+ this.insertSchemaSubschemas(schema[key], rootSchemaId)
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ function getDataByJSONPointer (data, jsonPointer) {
72
+ const parts = jsonPointer.split('/')
73
+ let current = data
74
+ for (const part of parts) {
75
+ if (part === '' || part === '#') continue
76
+ if (typeof current !== 'object' || current === null) {
77
+ return undefined
78
+ }
79
+ current = current[part]
80
+ }
81
+ return current
82
+ }
83
+
84
+ module.exports = RefResolver
package/serializer.js CHANGED
@@ -74,6 +74,9 @@ module.exports = class Serializer {
74
74
  if (date instanceof Date) {
75
75
  return '"' + date.toISOString() + '"'
76
76
  }
77
+ if (typeof date === 'string') {
78
+ return '"' + date + '"'
79
+ }
77
80
  throw new Error(`The value "${date}" cannot be converted to a date-time.`)
78
81
  }
79
82
 
@@ -86,6 +89,9 @@ module.exports = class Serializer {
86
89
  if (date instanceof Date) {
87
90
  return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"'
88
91
  }
92
+ if (typeof date === 'string') {
93
+ return '"' + date + '"'
94
+ }
89
95
  throw new Error(`The value "${date}" cannot be converted to a date.`)
90
96
  }
91
97
 
@@ -98,6 +104,9 @@ module.exports = class Serializer {
98
104
  if (date instanceof Date) {
99
105
  return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"'
100
106
  }
107
+ if (typeof date === 'string') {
108
+ return '"' + date + '"'
109
+ }
101
110
  throw new Error(`The value "${date}" cannot be converted to a time.`)
102
111
  }
103
112
 
package/standalone.js CHANGED
@@ -1,28 +1,28 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
3
 
4
- function buildStandaloneCode (options, ajvInstance, contextFunctionCode) {
4
+ function buildStandaloneCode (options, validator, contextFunctionCode) {
5
5
  const serializerCode = fs.readFileSync(path.join(__dirname, 'serializer.js')).toString()
6
6
  let buildAjvCode = ''
7
7
  let defaultAjvSchema = ''
8
- const defaultMeta = ajvInstance.defaultMeta()
8
+ const defaultMeta = validator.ajv.defaultMeta()
9
9
  if (typeof defaultMeta === 'string') {
10
10
  defaultAjvSchema = defaultMeta
11
11
  } else {
12
12
  defaultAjvSchema = defaultMeta.$id || defaultMeta.id
13
13
  }
14
- const shouldUseAjv = contextFunctionCode.indexOf('ajv') !== -1
14
+ const shouldUseAjv = contextFunctionCode.indexOf('validator') !== -1
15
15
  // we need to export the custom json schema
16
16
  let ajvSchemasCode = ''
17
17
  if (shouldUseAjv) {
18
- ajvSchemasCode += `const ajv = buildAjv(${JSON.stringify(options.ajv || {})})\n`
19
- for (const [id, schema] of Object.entries(ajvInstance.schemas)) {
18
+ ajvSchemasCode += `const validator = new Validator(${JSON.stringify(options.ajv || {})})\n`
19
+ for (const [id, schema] of Object.entries(validator.ajv.schemas)) {
20
20
  // should skip ajv default schema
21
21
  if (id === defaultAjvSchema) continue
22
- ajvSchemasCode += `ajv.addSchema(${JSON.stringify(schema.schema)}, "${id}")\n`
22
+ ajvSchemasCode += `validator.ajv.addSchema(${JSON.stringify(schema.schema)}, "${id}")\n`
23
23
  }
24
- buildAjvCode = fs.readFileSync(path.join(__dirname, 'ajv.js')).toString()
25
- buildAjvCode = buildAjvCode.replace("'use strict'", '').replace('module.exports = buildAjv', '')
24
+ buildAjvCode = fs.readFileSync(path.join(__dirname, 'validator.js')).toString()
25
+ buildAjvCode = buildAjvCode.replace("'use strict'", '').replace('module.exports = SchemaValidator', '')
26
26
  }
27
27
  return `
28
28
  'use strict'
@@ -24,6 +24,184 @@ test('schema with const string', (t) => {
24
24
  t.ok(validate(JSON.parse(output)), 'valid schema')
25
25
  })
26
26
 
27
+ test('schema with const string and different input', (t) => {
28
+ t.plan(2)
29
+
30
+ const schema = {
31
+ type: 'object',
32
+ properties: {
33
+ foo: { const: 'bar' }
34
+ }
35
+ }
36
+
37
+ const validate = validator(schema)
38
+ const stringify = build(schema)
39
+ const output = stringify({
40
+ foo: 'baz'
41
+ })
42
+
43
+ t.equal(output, '{"foo":"bar"}')
44
+ t.ok(validate(JSON.parse(output)), 'valid schema')
45
+ })
46
+
47
+ test('schema with const string and different type input', (t) => {
48
+ t.plan(2)
49
+
50
+ const schema = {
51
+ type: 'object',
52
+ properties: {
53
+ foo: { const: 'bar' }
54
+ }
55
+ }
56
+
57
+ const validate = validator(schema)
58
+ const stringify = build(schema)
59
+ const output = stringify({
60
+ foo: 1
61
+ })
62
+
63
+ t.equal(output, '{"foo":"bar"}')
64
+ t.ok(validate(JSON.parse(output)), 'valid schema')
65
+ })
66
+
67
+ test('schema with const string and no input', (t) => {
68
+ t.plan(2)
69
+
70
+ const schema = {
71
+ type: 'object',
72
+ properties: {
73
+ foo: { const: 'bar' }
74
+ }
75
+ }
76
+
77
+ const validate = validator(schema)
78
+ const stringify = build(schema)
79
+ const output = stringify({})
80
+
81
+ t.equal(output, '{}')
82
+ t.ok(validate(JSON.parse(output)), 'valid schema')
83
+ })
84
+
85
+ test('schema with const number', (t) => {
86
+ t.plan(2)
87
+
88
+ const schema = {
89
+ type: 'object',
90
+ properties: {
91
+ foo: { const: 1 }
92
+ }
93
+ }
94
+
95
+ const validate = validator(schema)
96
+ const stringify = build(schema)
97
+ const output = stringify({
98
+ foo: 1
99
+ })
100
+
101
+ t.equal(output, '{"foo":1}')
102
+ t.ok(validate(JSON.parse(output)), 'valid schema')
103
+ })
104
+
105
+ test('schema with const number and different input', (t) => {
106
+ t.plan(2)
107
+
108
+ const schema = {
109
+ type: 'object',
110
+ properties: {
111
+ foo: { const: 1 }
112
+ }
113
+ }
114
+
115
+ const validate = validator(schema)
116
+ const stringify = build(schema)
117
+ const output = stringify({
118
+ foo: 2
119
+ })
120
+
121
+ t.equal(output, '{"foo":1}')
122
+ t.ok(validate(JSON.parse(output)), 'valid schema')
123
+ })
124
+
125
+ test('schema with const bool', (t) => {
126
+ t.plan(2)
127
+
128
+ const schema = {
129
+ type: 'object',
130
+ properties: {
131
+ foo: { const: true }
132
+ }
133
+ }
134
+
135
+ const validate = validator(schema)
136
+ const stringify = build(schema)
137
+ const output = stringify({
138
+ foo: true
139
+ })
140
+
141
+ t.equal(output, '{"foo":true}')
142
+ t.ok(validate(JSON.parse(output)), 'valid schema')
143
+ })
144
+
145
+ test('schema with const number', (t) => {
146
+ t.plan(2)
147
+
148
+ const schema = {
149
+ type: 'object',
150
+ properties: {
151
+ foo: { const: 1 }
152
+ }
153
+ }
154
+
155
+ const validate = validator(schema)
156
+ const stringify = build(schema)
157
+ const output = stringify({
158
+ foo: 1
159
+ })
160
+
161
+ t.equal(output, '{"foo":1}')
162
+ t.ok(validate(JSON.parse(output)), 'valid schema')
163
+ })
164
+
165
+ test('schema with const null', (t) => {
166
+ t.plan(2)
167
+
168
+ const schema = {
169
+ type: 'object',
170
+ properties: {
171
+ foo: { const: null }
172
+ }
173
+ }
174
+
175
+ const validate = validator(schema)
176
+ const stringify = build(schema)
177
+ const output = stringify({
178
+ foo: null
179
+ })
180
+
181
+ t.equal(output, '{"foo":null}')
182
+ t.ok(validate(JSON.parse(output)), 'valid schema')
183
+ })
184
+
185
+ test('schema with const array', (t) => {
186
+ t.plan(2)
187
+
188
+ const schema = {
189
+ type: 'object',
190
+ properties: {
191
+ foo: { const: [1, 2, 3] }
192
+ }
193
+ }
194
+
195
+ const validate = validator(schema)
196
+ const stringify = build(schema)
197
+ const output = stringify({
198
+ foo: [1, 2, 3]
199
+ })
200
+
201
+ t.equal(output, '{"foo":[1,2,3]}')
202
+ t.ok(validate(JSON.parse(output)), 'valid schema')
203
+ })
204
+
27
205
  test('schema with const object', (t) => {
28
206
  t.plan(2)
29
207
 
@@ -44,6 +222,56 @@ test('schema with const object', (t) => {
44
222
  t.ok(validate(JSON.parse(output)), 'valid schema')
45
223
  })
46
224
 
225
+ test('schema with const and null as type', (t) => {
226
+ t.plan(4)
227
+
228
+ const schema = {
229
+ type: 'object',
230
+ properties: {
231
+ foo: { type: ['string', 'null'], const: 'baz' }
232
+ }
233
+ }
234
+
235
+ const validate = validator(schema)
236
+ const stringify = build(schema)
237
+ const output = stringify({
238
+ foo: null
239
+ })
240
+
241
+ t.equal(output, '{"foo":null}')
242
+ t.ok(validate(JSON.parse(output)), 'valid schema')
243
+
244
+ const output2 = stringify({ foo: 'baz' })
245
+ t.equal(output2, '{"foo":"baz"}')
246
+ t.ok(validate(JSON.parse(output2)), 'valid schema')
247
+ })
248
+
249
+ test('schema with const as nullable', (t) => {
250
+ t.plan(4)
251
+
252
+ const schema = {
253
+ type: 'object',
254
+ properties: {
255
+ foo: { nullable: true, const: 'baz' }
256
+ }
257
+ }
258
+
259
+ const validate = validator(schema)
260
+ const stringify = build(schema)
261
+ const output = stringify({
262
+ foo: null
263
+ })
264
+
265
+ t.equal(output, '{"foo":null}')
266
+ t.ok(validate(JSON.parse(output)), 'valid schema')
267
+
268
+ const output2 = stringify({
269
+ foo: 'baz'
270
+ })
271
+ t.equal(output2, '{"foo":"baz"}')
272
+ t.ok(validate(JSON.parse(output2)), 'valid schema')
273
+ })
274
+
47
275
  test('schema with const and invalid object', (t) => {
48
276
  t.plan(2)
49
277
 
@@ -55,13 +283,12 @@ test('schema with const and invalid object', (t) => {
55
283
  required: ['foo']
56
284
  }
57
285
 
286
+ const validate = validator(schema)
58
287
  const stringify = build(schema)
59
- try {
60
- stringify({
61
- foo: { foo: 'baz' }
62
- })
63
- } catch (err) {
64
- t.match(err.message, /^Item .* does not match schema definition/, 'Given object has invalid const value')
65
- t.ok(err)
66
- }
288
+ const result = stringify({
289
+ foo: { foo: 'baz' }
290
+ })
291
+
292
+ t.equal(result, '{"foo":{"foo":"bar"}}')
293
+ t.ok(validate(JSON.parse(result)), 'valid schema')
67
294
  })
@@ -2,7 +2,9 @@
2
2
 
3
3
  const test = require('tap').test
4
4
  const fjs = require('..')
5
+
5
6
  const Ajv = require('ajv').default
7
+ const Validator = require('../validator')
6
8
 
7
9
  function build (opts) {
8
10
  return fjs({
@@ -18,31 +20,34 @@ function build (opts) {
18
20
  }
19
21
 
20
22
  test('activate debug mode', t => {
21
- t.plan(3)
23
+ t.plan(4)
22
24
  const debugMode = build({ debugMode: true })
23
25
 
24
26
  t.type(debugMode, 'object')
25
27
  t.ok(debugMode.ajv instanceof Ajv)
28
+ t.ok(debugMode.validator instanceof Validator)
26
29
  t.type(debugMode.code, 'string')
27
30
  })
28
31
 
29
32
  test('activate debug mode truthy', t => {
30
- t.plan(3)
33
+ t.plan(4)
31
34
 
32
35
  const debugMode = build({ debugMode: 'yes' })
33
36
 
34
37
  t.type(debugMode, 'object')
35
38
  t.type(debugMode.code, 'string')
36
39
  t.ok(debugMode.ajv instanceof Ajv)
40
+ t.ok(debugMode.validator instanceof Validator)
37
41
  })
38
42
 
39
43
  test('to string auto-consistent', t => {
40
- t.plan(4)
44
+ t.plan(5)
41
45
  const debugMode = build({ debugMode: 1 })
42
46
 
43
47
  t.type(debugMode, 'object')
44
48
  t.type(debugMode.code, 'string')
45
49
  t.ok(debugMode.ajv instanceof Ajv)
50
+ t.ok(debugMode.validator instanceof Validator)
46
51
 
47
52
  const compiled = fjs.restore(debugMode)
48
53
  const tobe = JSON.stringify({ firstName: 'Foo' })
@@ -50,7 +55,7 @@ test('to string auto-consistent', t => {
50
55
  })
51
56
 
52
57
  test('to string auto-consistent with ajv', t => {
53
- t.plan(4)
58
+ t.plan(5)
54
59
 
55
60
  const debugMode = fjs({
56
61
  title: 'object with multiple types field',
@@ -69,6 +74,7 @@ test('to string auto-consistent with ajv', t => {
69
74
  t.type(debugMode, 'object')
70
75
  t.type(debugMode.code, 'string')
71
76
  t.ok(debugMode.ajv instanceof Ajv)
77
+ t.ok(debugMode.validator instanceof Validator)
72
78
 
73
79
  const compiled = fjs.restore(debugMode)
74
80
  const tobe = JSON.stringify({ str: 'Foo' })
@@ -444,3 +444,100 @@ test('large array of nullable booleans with default mechanism', (t) => {
444
444
  t.same(result, JSON.stringify(data))
445
445
  t.same(JSON.parse(result), data)
446
446
  })
447
+
448
+ test('nullable type in the schema', (t) => {
449
+ t.plan(2)
450
+
451
+ const schema = {
452
+ type: ['object', 'null'],
453
+ properties: {
454
+ foo: {
455
+ type: 'string'
456
+ }
457
+ }
458
+ }
459
+
460
+ const stringify = build(schema)
461
+
462
+ const data = { foo: 'bar' }
463
+
464
+ t.same(stringify(data), JSON.stringify(data))
465
+ t.same(stringify(null), JSON.stringify(null))
466
+ })
467
+
468
+ test('throw an error if the value doesn\'t match the type', (t) => {
469
+ t.plan(2)
470
+
471
+ const schema = {
472
+ type: 'object',
473
+ additionalProperties: false,
474
+ required: ['data'],
475
+ properties: {
476
+ data: {
477
+ type: 'array',
478
+ minItems: 1,
479
+ items: {
480
+ oneOf: [
481
+ {
482
+ type: 'string'
483
+ },
484
+ {
485
+ type: 'number'
486
+ }
487
+ ]
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ const stringify = build(schema)
494
+
495
+ const validData = { data: [1, 'testing'] }
496
+ t.equal(stringify(validData), JSON.stringify(validData))
497
+
498
+ const invalidData = { data: [false, 'testing'] }
499
+ t.throws(() => stringify(invalidData))
500
+ })
501
+
502
+ test('nullable value in oneOf', (t) => {
503
+ t.plan(1)
504
+
505
+ const schema = {
506
+ type: 'object',
507
+ properties: {
508
+ data: {
509
+ oneOf: [
510
+ {
511
+ type: 'array',
512
+ items: {
513
+ type: 'object',
514
+ properties: {
515
+ id: { type: 'integer', minimum: 1 }
516
+ },
517
+ additionalProperties: false,
518
+ required: ['id']
519
+ }
520
+ },
521
+ {
522
+ type: 'array',
523
+ items: {
524
+ type: 'object',
525
+ properties: {
526
+ job: { type: 'string', nullable: true }
527
+ },
528
+ additionalProperties: false,
529
+ required: ['job']
530
+ }
531
+ }
532
+ ]
533
+ }
534
+ },
535
+ required: ['data'],
536
+ additionalProperties: false
537
+ }
538
+
539
+ const stringify = build(schema)
540
+
541
+ const data = { data: [{ job: null }] }
542
+ t.equal(stringify(data), JSON.stringify(data))
543
+ })
package/test/ref.test.js CHANGED
@@ -1953,3 +1953,31 @@ test('should resolve absolute $refs', (t) => {
1953
1953
 
1954
1954
  t.equal(output, JSON.stringify(object))
1955
1955
  })
1956
+
1957
+ test('nested schema should overwrite anchor scope', (t) => {
1958
+ t.plan(2)
1959
+
1960
+ const externalSchema = {
1961
+ root: {
1962
+ $id: 'root',
1963
+ definitions: {
1964
+ subschema: {
1965
+ $id: 'subschema',
1966
+ definitions: {
1967
+ anchorSchema: {
1968
+ $id: '#anchor',
1969
+ type: 'string'
1970
+ }
1971
+ }
1972
+ }
1973
+ }
1974
+ }
1975
+ }
1976
+
1977
+ const data = 'test'
1978
+ const stringify = build({ $ref: 'subschema#anchor' }, { schema: externalSchema })
1979
+ const output = stringify(data)
1980
+
1981
+ t.equal(output, JSON.stringify(data))
1982
+ t.throws(() => build({ $ref: 'root#anchor' }, { schema: externalSchema }))
1983
+ })
@@ -438,6 +438,35 @@ test('object that is simultaneously a string and a json switched', (t) => {
438
438
  t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
439
439
  })
440
440
 
441
+ test('class instance that is simultaneously a string and a json', (t) => {
442
+ t.plan(2)
443
+
444
+ const schema = {
445
+ type: 'object',
446
+ properties: {
447
+ simultaneously: {
448
+ type: ['string', 'object'],
449
+ properties: {
450
+ foo: { type: 'string' }
451
+ }
452
+ }
453
+ }
454
+ }
455
+
456
+ class Test {
457
+ toString () { return 'hello' }
458
+ }
459
+
460
+ const likeObjectId = new Test()
461
+
462
+ const stringify = build(schema)
463
+ const valueStr = stringify({ simultaneously: likeObjectId })
464
+ t.equal(valueStr, '{"simultaneously":"hello"}')
465
+
466
+ const valueObj = stringify({ simultaneously: { foo: likeObjectId } })
467
+ t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
468
+ })
469
+
441
470
  test('should throw an error when type is array and object is null', (t) => {
442
471
  t.plan(1)
443
472
  const schema = {
package/validator.js ADDED
@@ -0,0 +1,75 @@
1
+ 'use strict'
2
+
3
+ const Ajv = require('ajv')
4
+ const fastUri = require('fast-uri')
5
+ const ajvFormats = require('ajv-formats')
6
+ const clone = require('rfdc')({ proto: true })
7
+
8
+ class Validator {
9
+ constructor (ajvOptions) {
10
+ this.ajv = new Ajv({
11
+ ...ajvOptions,
12
+ strictSchema: false,
13
+ validateSchema: false,
14
+ allowUnionTypes: true,
15
+ uriResolver: fastUri
16
+ })
17
+
18
+ ajvFormats(this.ajv)
19
+
20
+ this.ajv.addKeyword({
21
+ keyword: 'fjs_type',
22
+ type: 'object',
23
+ errors: false,
24
+ validate: (type, date) => {
25
+ return date instanceof Date
26
+ }
27
+ })
28
+ }
29
+
30
+ addSchema (schema, schemaName) {
31
+ let schemaKey = schema.$id || schemaName
32
+ if (schema.$id !== undefined && schema.$id[0] === '#') {
33
+ schemaKey = schemaName + schema.$id // relative URI
34
+ }
35
+
36
+ if (
37
+ this.ajv.refs[schemaKey] === undefined &&
38
+ this.ajv.schemas[schemaKey] === undefined
39
+ ) {
40
+ const ajvSchema = clone(schema)
41
+ this.convertSchemaToAjvFormat(ajvSchema)
42
+ this.ajv.addSchema(ajvSchema, schemaKey)
43
+ }
44
+ }
45
+
46
+ validate (schemaRef, data) {
47
+ return this.ajv.validate(schemaRef, data)
48
+ }
49
+
50
+ // Ajv does not support js date format. In order to properly validate objects containing a date,
51
+ // it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
52
+ // (see https://github.com/fastify/fast-json-stringify/pull/441)
53
+ convertSchemaToAjvFormat (schema) {
54
+ if (schema === null) return
55
+
56
+ if (schema.type === 'string') {
57
+ schema.fjs_type = 'string'
58
+ schema.type = ['string', 'object']
59
+ } else if (
60
+ Array.isArray(schema.type) &&
61
+ schema.type.includes('string') &&
62
+ !schema.type.includes('object')
63
+ ) {
64
+ schema.fjs_type = 'string'
65
+ schema.type.push('object')
66
+ }
67
+ for (const property in schema) {
68
+ if (typeof schema[property] === 'object') {
69
+ this.convertSchemaToAjvFormat(schema[property])
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ module.exports = Validator
package/ajv.js DELETED
@@ -1,30 +0,0 @@
1
- 'use strict'
2
-
3
- const Ajv = require('ajv')
4
- const fastUri = require('fast-uri')
5
- const ajvFormats = require('ajv-formats')
6
-
7
- module.exports = buildAjv
8
-
9
- function buildAjv (options) {
10
- const ajvInstance = new Ajv({
11
- ...options,
12
- strictSchema: false,
13
- validateSchema: false,
14
- allowUnionTypes: true,
15
- uriResolver: fastUri
16
- })
17
-
18
- ajvFormats(ajvInstance)
19
-
20
- ajvInstance.addKeyword({
21
- keyword: 'fjs_type',
22
- type: 'object',
23
- errors: false,
24
- validate: (type, date) => {
25
- return date instanceof Date
26
- }
27
- })
28
-
29
- return ajvInstance
30
- }