fast-json-stringify 7.0.0 → 7.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.
Files changed (39) hide show
  1. package/.github/dependabot.yml +43 -2
  2. package/.github/workflows/ci.yml +1 -1
  3. package/.github/workflows/lock-threads.yml +1 -1
  4. package/.github/workflows/release.yml +24 -0
  5. package/README.md +25 -8
  6. package/benchmark/bench-thread.js +6 -2
  7. package/benchmark/bench.js +86 -0
  8. package/index.js +360 -94
  9. package/lib/location.js +3 -1
  10. package/lib/serializer.js +41 -37
  11. package/lib/standalone.js +22 -3
  12. package/lib/validator.js +22 -3
  13. package/package.json +6 -7
  14. package/test/additionalProperties.test.js +87 -0
  15. package/test/anyof.test.js +42 -0
  16. package/test/array.test.js +1 -1
  17. package/test/basic.test.js +1 -1
  18. package/test/code-generation-fallbacks.test.js +158 -0
  19. package/test/compile-validators.test.js +110 -0
  20. package/test/const.test.js +27 -6
  21. package/test/date.test.js +28 -1
  22. package/test/debug-mode.test.js +8 -0
  23. package/test/deopt.test.js +251 -0
  24. package/test/if-then-else.test.js +145 -0
  25. package/test/inferType.test.js +1 -1
  26. package/test/integer.test.js +1 -1
  27. package/test/oneof.test.js +242 -0
  28. package/test/optional-property-prefix.test.js +119 -0
  29. package/test/patternProperties.test.js +24 -0
  30. package/test/property-names.test.js +41 -0
  31. package/test/regex.test.js +1 -1
  32. package/test/schema-depth.test.js +227 -0
  33. package/test/standalone-mode.test.js +109 -2
  34. package/test/string.test.js +8 -0
  35. package/test/surrogate.test.js +1 -1
  36. package/test/typesArray.test.js +59 -0
  37. package/test/utils/validator.js +20 -0
  38. package/types/index.d.ts +20 -0
  39. package/types/index.tst.ts +13 -0
package/index.js CHANGED
@@ -10,11 +10,35 @@ const Location = require('./lib/location')
10
10
  const validate = require('./lib/schema-validator')
11
11
  const mergeSchemas = require('./lib/merge-schemas')
12
12
 
13
- const SINGLE_TICK = /'/g
14
-
15
13
  let largeArraySize = 2e4
16
14
  let largeArrayMechanism = 'default'
17
15
 
16
+ const DEFAULT_MAX_DEPTH = 100
17
+ const NAMED_FRAGMENT_REF = /^#[a-z_][-\w._]*$/i
18
+
19
+ const singleSchemaKeywords = [
20
+ 'additionalItems',
21
+ 'additionalProperties',
22
+ 'contains',
23
+ 'else',
24
+ 'if',
25
+ 'not',
26
+ 'propertyNames',
27
+ 'then'
28
+ ]
29
+
30
+ const arraySchemaKeywords = [
31
+ 'allOf',
32
+ 'anyOf',
33
+ 'oneOf'
34
+ ]
35
+
36
+ const objectSchemaKeywords = [
37
+ 'definitions',
38
+ 'patternProperties',
39
+ 'properties'
40
+ ]
41
+
18
42
  const serializerFns = `
19
43
  const {
20
44
  asString,
@@ -44,6 +68,113 @@ const validLargeArrayMechanisms = new Set([
44
68
 
45
69
  let schemaIdCounter = 0
46
70
 
71
+ function getMaxDepth (options) {
72
+ const maxDepth = options.maxDepth === undefined ? DEFAULT_MAX_DEPTH : options.maxDepth
73
+ if (!Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > DEFAULT_MAX_DEPTH) {
74
+ throw new Error(`Unsupported max schema depth ${maxDepth}. Expected an integer between 0 and ${DEFAULT_MAX_DEPTH}.`)
75
+ }
76
+ return maxDepth
77
+ }
78
+
79
+ function schemaDepthError (maxDepth, name) {
80
+ const prefix = name ? `"${name}" ` : ''
81
+ return new Error(`${prefix}schema exceeds maximum depth of ${maxDepth}`)
82
+ }
83
+
84
+ function assertObjectDepth (schema, maxDepth, name) {
85
+ const stack = [{ value: schema, depth: 0, exit: false }]
86
+ const path = new WeakSet()
87
+
88
+ while (stack.length > 0) {
89
+ const entry = stack.pop()
90
+ const value = entry.value
91
+
92
+ if (typeof value !== 'object' || value === null) continue
93
+
94
+ // Schema maps and arrays add at most one container between schema nodes.
95
+ // Keep dependency traversals within the same stack-safety envelope while
96
+ // still allowing maxDepth schema nodes nested through `properties`.
97
+ if (entry.depth > maxDepth * 2) {
98
+ throw schemaDepthError(maxDepth, name)
99
+ }
100
+
101
+ if (entry.exit) {
102
+ path.delete(value)
103
+ continue
104
+ }
105
+
106
+ if (path.has(value)) {
107
+ throw new Error('schema contains a circular object reference; use $ref instead')
108
+ }
109
+ path.add(value)
110
+ stack.push({ value, depth: entry.depth, exit: true })
111
+
112
+ for (const key in value) {
113
+ stack.push({ value: value[key], depth: entry.depth + 1, exit: false })
114
+ }
115
+ }
116
+ }
117
+
118
+ function assertSchemaDepth (schema, maxDepth, name) {
119
+ const stack = [{ schema, depth: 0 }]
120
+
121
+ while (stack.length > 0) {
122
+ const entry = stack.pop()
123
+ const currentSchema = entry.schema
124
+
125
+ if (entry.depth > maxDepth) {
126
+ throw schemaDepthError(maxDepth, name)
127
+ }
128
+
129
+ if (typeof currentSchema !== 'object' || currentSchema === null) continue
130
+
131
+ const nestedDepth = entry.depth + 1
132
+
133
+ for (const keyword of singleSchemaKeywords) {
134
+ if (typeof currentSchema[keyword] === 'object' && currentSchema[keyword] !== null) {
135
+ stack.push({ schema: currentSchema[keyword], depth: nestedDepth })
136
+ }
137
+ }
138
+
139
+ for (const keyword of arraySchemaKeywords) {
140
+ const schemas = currentSchema[keyword]
141
+ if (Array.isArray(schemas)) {
142
+ for (const nestedSchema of schemas) {
143
+ stack.push({ schema: nestedSchema, depth: nestedDepth })
144
+ }
145
+ }
146
+ }
147
+
148
+ const items = currentSchema.items
149
+ if (Array.isArray(items)) {
150
+ for (const nestedSchema of items) {
151
+ stack.push({ schema: nestedSchema, depth: nestedDepth })
152
+ }
153
+ } else if (typeof items === 'object' && items !== null) {
154
+ stack.push({ schema: items, depth: nestedDepth })
155
+ }
156
+
157
+ for (const keyword of objectSchemaKeywords) {
158
+ const schemas = currentSchema[keyword]
159
+ if (typeof schemas === 'object' && schemas !== null) {
160
+ for (const key of Object.keys(schemas)) {
161
+ stack.push({ schema: schemas[key], depth: nestedDepth })
162
+ }
163
+ }
164
+ }
165
+
166
+ const dependencies = currentSchema.dependencies
167
+ if (typeof dependencies === 'object' && dependencies !== null) {
168
+ for (const key of Object.keys(dependencies)) {
169
+ const dependency = dependencies[key]
170
+ if (!Array.isArray(dependency)) {
171
+ stack.push({ schema: dependency, depth: nestedDepth })
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+
47
178
  function isValidSchema (schema, name) {
48
179
  if (!validate(schema)) {
49
180
  if (name) {
@@ -59,27 +190,64 @@ function isValidSchema (schema, name) {
59
190
  }
60
191
 
61
192
  function resolveRef (context, location) {
62
- const ref = location.schema.$ref
193
+ const seen = new Set()
194
+ let depth = 0
63
195
 
64
- let hashIndex = ref.indexOf('#')
65
- if (hashIndex === -1) {
66
- hashIndex = ref.length
67
- }
196
+ while (location.schema.$ref !== undefined) {
197
+ const ref = location.schema.$ref
198
+ const locationRef = location.getSchemaRef()
68
199
 
69
- const schemaId = ref.slice(0, hashIndex) || location.schemaId
70
- const jsonPointer = ref.slice(hashIndex) || '#'
200
+ if (seen.has(locationRef)) {
201
+ throw new Error(`Cannot resolve circular reference "${ref}"`)
202
+ }
203
+ seen.add(locationRef)
204
+
205
+ if (depth++ > context.maxDepth) {
206
+ throw schemaDepthError(context.maxDepth)
207
+ }
208
+
209
+ let hashIndex = ref.indexOf('#')
210
+ if (hashIndex === -1) {
211
+ hashIndex = ref.length
212
+ }
213
+
214
+ const schemaId = ref.slice(0, hashIndex) || location.schemaId
215
+ const jsonPointer = ref.slice(hashIndex) || '#'
216
+
217
+ const schema = context.refResolver.getSchema(schemaId, jsonPointer)
218
+ if (schema === null) {
219
+ throw new Error(`Cannot find reference "${ref}"`)
220
+ }
71
221
 
72
- const schema = context.refResolver.getSchema(schemaId, jsonPointer)
73
- if (schema === null) {
74
- throw new Error(`Cannot find reference "${ref}"`)
222
+ location = new Location(schema, schemaId, jsonPointer)
75
223
  }
76
224
 
77
- const newLocation = new Location(schema, schemaId, jsonPointer)
78
- if (schema.$ref !== undefined) {
79
- return resolveRef(context, newLocation)
225
+ return location
226
+ }
227
+
228
+ function getSchemaDependencies (refResolver, schemaId) {
229
+ const dependencies = {}
230
+ const processed = new Set()
231
+ const pending = [schemaId]
232
+
233
+ while (pending.length > 0) {
234
+ const currentSchemaId = pending.pop()
235
+ if (processed.has(currentSchemaId)) continue
236
+ processed.add(currentSchemaId)
237
+
238
+ for (const ref of refResolver.getSchemaRefs(currentSchemaId)) {
239
+ const dependencySchemaId = ref.schemaId
240
+ if (
241
+ dependencySchemaId === currentSchemaId ||
242
+ dependencies[dependencySchemaId] !== undefined
243
+ ) continue
244
+
245
+ dependencies[dependencySchemaId] = refResolver.getSchema(dependencySchemaId)
246
+ pending.push(dependencySchemaId)
247
+ }
80
248
  }
81
249
 
82
- return newLocation
250
+ return dependencies
83
251
  }
84
252
 
85
253
  function getMergedLocation (context, mergedSchemaId) {
@@ -94,6 +262,14 @@ function getSchemaId (schema, rootSchemaId) {
94
262
  return rootSchemaId
95
263
  }
96
264
 
265
+ // a schema ref on a comment line of the generated code: a line terminator in a property name
266
+ // would end the comment and turn the rest of the name into code. JSON.stringify covers \n and
267
+ // \r, the two separators JS also treats as line terminators are escaped by hand
268
+ const LINE_SEPARATORS = /[\u2028\u2029]/g
269
+ function asComment (schemaRef) {
270
+ return JSON.stringify(schemaRef).replace(LINE_SEPARATORS, (c) => '\\u' + c.charCodeAt(0).toString(16))
271
+ }
272
+
97
273
  function getSafeSchemaRef (context, location) {
98
274
  let schemaRef = location.getSchemaRef() || ''
99
275
  if (schemaRef.startsWith(context.rootSchemaId)) {
@@ -102,11 +278,59 @@ function getSafeSchemaRef (context, location) {
102
278
  return schemaRef
103
279
  }
104
280
 
105
- function build (schema, options) {
106
- isValidSchema(schema)
281
+ function getValidatorSchemaRef (context, location) {
282
+ let schemaRef = location.getSchemaRef()
283
+ const schema = location.schema
284
+
285
+ // A schema that is only a registered $ref can validate against that target
286
+ // instead of a JSON-pointer path through the root schema.
287
+ if (
288
+ typeof schema.$ref !== 'string' ||
289
+ Object.keys(schema).length !== 1
290
+ ) {
291
+ return schemaRef
292
+ }
293
+
294
+ const ref = schema.$ref
295
+ const hashIndex = ref.indexOf('#')
296
+ const refSchemaId = hashIndex === -1 ? ref : ref.slice(0, hashIndex)
297
+ const refJsonPointer = hashIndex === -1 ? '' : ref.slice(hashIndex)
298
+ const isBareRef = refJsonPointer === '' || refJsonPointer === '#'
299
+ const isNamedFragmentRef = NAMED_FRAGMENT_REF.test(refJsonPointer)
300
+ const isJsonPointerRef = refJsonPointer.startsWith('#/')
301
+
302
+ if (
303
+ (isBareRef || isNamedFragmentRef || isJsonPointerRef) &&
304
+ refSchemaId &&
305
+ context.refResolver.hasSchema(refSchemaId)
306
+ ) {
307
+ const refSchema = context.refResolver.getSchema(refSchemaId)
308
+ const refSubSchema = isBareRef
309
+ ? refSchema
310
+ : context.refResolver.getSchema(refSchemaId, refJsonPointer)
311
+ const hasRelativeSchemaId = (
312
+ typeof refSchema.$id === 'string' &&
313
+ refSchema.$id.charAt(0) === '#'
314
+ )
315
+ if (
316
+ refSubSchema !== null &&
317
+ (!hasRelativeSchemaId || refSchema.$id === refJsonPointer)
318
+ ) {
319
+ schemaRef = refSchemaId + (isBareRef ? '#' : refJsonPointer)
320
+ }
321
+ }
107
322
 
323
+ return schemaRef
324
+ }
325
+
326
+ function build (schema, options) {
108
327
  options = options || {}
109
328
 
329
+ const maxDepth = getMaxDepth(options)
330
+ assertObjectDepth(schema, maxDepth)
331
+ assertSchemaDepth(schema, maxDepth)
332
+ isValidSchema(schema)
333
+
110
334
  const context = {
111
335
  functions: [],
112
336
  functionsCounter: 0,
@@ -115,7 +339,9 @@ function build (schema, options) {
115
339
  refResolver: new RefResolver(),
116
340
  rootSchemaId: schema.$id || `__fjs_root_${schemaIdCounter++}`,
117
341
  validatorSchemasIds: new Set(),
342
+ validatorSchemaRefs: new Set(),
118
343
  mergedSchemasIds: new Map(),
344
+ maxDepth,
119
345
  recursiveSchemas: new Set(),
120
346
  recursivePaths: new Set(),
121
347
  buildingSet: new Set(),
@@ -132,6 +358,8 @@ function build (schema, options) {
132
358
  const schema = options.schema[key]
133
359
  const schemaId = getSchemaId(schema, key)
134
360
  if (!context.refResolver.hasSchema(schemaId)) {
361
+ assertObjectDepth(schema, maxDepth, key)
362
+ assertSchemaDepth(schema, maxDepth, key)
135
363
  isValidSchema(schema, key)
136
364
  context.refResolver.addSchema(schema, key)
137
365
  }
@@ -210,18 +438,25 @@ function build (schema, options) {
210
438
  }
211
439
 
212
440
  const serializer = new Serializer(options)
213
- const validator = new Validator(options.ajv)
441
+ const validator = new Validator(
442
+ options.ajv,
443
+ options.mode === 'standalone' && options.inlineValidators
444
+ )
214
445
 
215
446
  for (const schemaId of context.validatorSchemasIds) {
216
447
  const schema = context.refResolver.getSchema(schemaId)
217
448
  validator.addSchema(schema, schemaId)
218
449
 
219
- const dependencies = context.refResolver.getSchemaDependencies(schemaId)
450
+ const dependencies = getSchemaDependencies(context.refResolver, schemaId)
220
451
  for (const [schemaId, schema] of Object.entries(dependencies)) {
221
452
  validator.addSchema(schema, schemaId)
222
453
  }
223
454
  }
224
455
 
456
+ if (options.compileValidators) {
457
+ validator.compileSchemas(context.validatorSchemaRefs)
458
+ }
459
+
225
460
  if (options.mode === 'debug') {
226
461
  return {
227
462
  validator,
@@ -331,7 +566,12 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma, objV
331
566
  const additionalPropertiesLocation = location.getPropertyLocation('additionalProperties')
332
567
  const additionalPropertiesSchema = additionalPropertiesLocation.schema
333
568
 
334
- if (additionalPropertiesSchema !== undefined) {
569
+ // `additionalProperties: false` means every property that is not declared in
570
+ // `properties` nor matched by `patternProperties` is dropped, so no branch is
571
+ // emitted for it. Without this guard the `false` schema reaches buildValue,
572
+ // which serializes any boolean schema with `JSON.stringify(value)` and lets
573
+ // the property through.
574
+ if (additionalPropertiesSchema !== undefined && additionalPropertiesSchema !== false) {
335
575
  if (additionalPropertiesSchema === true) {
336
576
  code += `
337
577
  ${addComma}
@@ -382,10 +622,9 @@ function buildInnerObject (context, location, objVar) {
382
622
  const localUid = context.uid++
383
623
  let addComma = ''
384
624
 
385
- if (requiredProperties.length > 0) {
386
- // If we have required properties, we know that at least one property will be serialized.
387
- // We can avoid the runtime check for the comma.
388
-
625
+ // propertiesKeys is sorted required-first; the guard checks [0] is required because
626
+ // otherwise additionalProperties/patternProperties would emit a stray `{ ,"k":v }`.
627
+ if (propertiesKeys.length > 0 && requiredProperties.includes(propertiesKeys[0])) {
389
628
  // The first property is required, so we don't need a comma.
390
629
  // For the subsequent properties, we can blindly add a comma.
391
630
 
@@ -450,12 +689,17 @@ function buildInnerObject (context, location, objVar) {
450
689
  const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
451
690
  const defaultValue = propertyLocation.schema.default
452
691
  const isRequired = requiredProperties.includes(key) // Should be false here but good to keep
692
+ // Select a complete prefix so the comma does not need a separate concatenation.
693
+ const propertyPrefix = sanitizedKey + ':'
694
+ const addProperty = needsRuntimeComma
695
+ ? `json += addComma_${localUid} ? ${JSON.stringify(',' + propertyPrefix)} : ${JSON.stringify(propertyPrefix)}
696
+ addComma_${localUid} = true`
697
+ : `json += ${JSON.stringify(propertyPrefix)}`
453
698
 
454
699
  code += `
455
700
  const ${value} = ${objVar}[${sanitizedKey}]
456
701
  if (${value} !== undefined) {
457
- ${addComma}
458
- json += ${JSON.stringify(sanitizedKey + ':')}
702
+ ${addProperty}
459
703
  ${buildValue(context, propertyLocation, `${value}`)}
460
704
  }`
461
705
 
@@ -571,7 +815,7 @@ function buildObject (context, location, input) {
571
815
  const schemaRef = getSafeSchemaRef(context, location)
572
816
 
573
817
  const functionCode = `
574
- // ${schemaRef}
818
+ // ${asComment(schemaRef)}
575
819
  function ${functionName} (input) {
576
820
  const obj = ${toJSON('input')}
577
821
  if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
@@ -631,14 +875,14 @@ function buildArray (context, location, input) {
631
875
 
632
876
  let functionCode = `
633
877
  function ${functionName} (obj) {
634
- // ${schemaRef}
878
+ // ${asComment(schemaRef)}
635
879
  let json = ''
636
880
  `
637
881
 
638
882
  functionCode += `
639
883
  if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
640
884
  if (!Array.isArray(obj)) {
641
- throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
885
+ throw new TypeError(${JSON.stringify(`The value of '${schemaRef}' does not match schema definition.`)})
642
886
  }
643
887
  const arrayLength = obj.length
644
888
  `
@@ -723,7 +967,7 @@ function buildArray (context, location, input) {
723
967
  if (${objVar} === null) {
724
968
  json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
725
969
  } else if (!Array.isArray(${objVar})) {
726
- throw new TypeError(\`The value of '${safeSchemaRef}' does not match schema definition.\`)
970
+ throw new TypeError(${JSON.stringify(`The value of '${safeSchemaRef}' does not match schema definition.`)})
727
971
  } else {
728
972
  const arrayLength_${objVar} = ${objVar}.length
729
973
  `
@@ -903,6 +1147,17 @@ function buildMultiTypeSerializer (context, location, input) {
903
1147
  `
904
1148
  break
905
1149
  }
1150
+ case 'object': {
1151
+ // An array is `typeof === 'object'`, so it would otherwise be captured
1152
+ // by this branch and serialized as an object (dropping its items). Exclude
1153
+ // arrays here so a sibling `array` type in the same `type` list can match.
1154
+ code += `
1155
+ ${statement}((typeof ${input} === "object" && !Array.isArray(${input})) || ${input} === null) {
1156
+ ${nestedResult}
1157
+ }
1158
+ `
1159
+ break
1160
+ }
906
1161
  default: {
907
1162
  code += `
908
1163
  ${statement}(typeof ${input} === "${type}" || ${input} === null) {
@@ -914,7 +1169,7 @@ function buildMultiTypeSerializer (context, location, input) {
914
1169
  }
915
1170
  })
916
1171
  code += `
917
- else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
1172
+ else throw new TypeError(${JSON.stringify(`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.`)})
918
1173
  `
919
1174
 
920
1175
  return code
@@ -974,9 +1229,13 @@ function buildSingleTypeSerializer (context, location, input) {
974
1229
 
975
1230
  function detectRecursiveSchemas (context, location) {
976
1231
  const pathStack = new Set()
977
- function traverse (location) {
1232
+ function traverse (location, depth) {
978
1233
  const schema = location.schema
979
- if (typeof schema !== 'object' || schema === null) return
1234
+ if (!schema || typeof schema !== 'object') return
1235
+
1236
+ if (depth > context.maxDepth) {
1237
+ throw schemaDepthError(context.maxDepth)
1238
+ }
980
1239
 
981
1240
  const schemaId = location.schemaId || ''
982
1241
  const jsonPointer = location.jsonPointer || ''
@@ -997,65 +1256,66 @@ function detectRecursiveSchemas (context, location) {
997
1256
  if (schema.$ref) {
998
1257
  try {
999
1258
  const res = resolveRef(context, location)
1000
- traverse(res)
1259
+ traverse(res, depth)
1001
1260
  } catch (err) {
1261
+ if (!err.message.startsWith('Cannot find reference')) throw err
1002
1262
  // Validation will handle missing refs later
1003
1263
  }
1004
1264
  }
1005
1265
 
1006
- if (schema.properties) {
1007
- const propertiesLocation = location.getPropertyLocation('properties')
1008
- for (const key in schema.properties) {
1009
- traverse(propertiesLocation.getPropertyLocation(key))
1010
- }
1011
- }
1012
- if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
1013
- traverse(location.getPropertyLocation('additionalProperties'))
1014
- }
1015
- if (schema.patternProperties) {
1016
- const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
1017
- for (const key in schema.patternProperties) {
1018
- traverse(patternPropertiesLocation.getPropertyLocation(key))
1266
+ const nestedDepth = depth + 1
1267
+
1268
+ for (const keyword of singleSchemaKeywords) {
1269
+ if (typeof schema[keyword] === 'object' && schema[keyword] !== null) {
1270
+ traverse(location.getPropertyLocation(keyword), nestedDepth)
1019
1271
  }
1020
1272
  }
1021
- if (schema.items) {
1022
- const itemsLocation = location.getPropertyLocation('items')
1023
- if (Array.isArray(schema.items)) {
1024
- for (let i = 0; i < schema.items.length; i++) {
1025
- traverse(itemsLocation.getPropertyLocation(i))
1273
+
1274
+ for (const keyword of arraySchemaKeywords) {
1275
+ const schemas = schema[keyword]
1276
+ if (Array.isArray(schemas)) {
1277
+ const schemasLocation = location.getPropertyLocation(keyword)
1278
+ const schemasLength = schemas.length
1279
+ for (let i = 0; i < schemasLength; i++) {
1280
+ traverse(schemasLocation.getPropertyLocation(i), nestedDepth)
1026
1281
  }
1027
- } else {
1028
- traverse(itemsLocation)
1029
1282
  }
1030
1283
  }
1031
- if (schema.additionalItems && typeof schema.additionalItems === 'object') {
1032
- traverse(location.getPropertyLocation('additionalItems'))
1033
- }
1034
1284
 
1035
- if (schema.oneOf) {
1036
- const oneOfLocation = location.getPropertyLocation('oneOf')
1037
- for (let i = 0; i < schema.oneOf.length; i++) {
1038
- traverse(oneOfLocation.getPropertyLocation(i))
1285
+ const items = schema.items
1286
+ if (Array.isArray(items)) {
1287
+ const itemsLocation = location.getPropertyLocation('items')
1288
+ const itemsLength = items.length
1289
+ for (let i = 0; i < itemsLength; i++) {
1290
+ traverse(itemsLocation.getPropertyLocation(i), nestedDepth)
1039
1291
  }
1292
+ } else if (typeof items === 'object' && items !== null) {
1293
+ traverse(location.getPropertyLocation('items'), nestedDepth)
1040
1294
  }
1041
- if (schema.anyOf) {
1042
- const anyOfLocation = location.getPropertyLocation('anyOf')
1043
- for (let i = 0; i < schema.anyOf.length; i++) {
1044
- traverse(anyOfLocation.getPropertyLocation(i))
1295
+
1296
+ for (const keyword of objectSchemaKeywords) {
1297
+ const schemas = schema[keyword]
1298
+ if (typeof schemas === 'object' && schemas !== null) {
1299
+ const schemasLocation = location.getPropertyLocation(keyword)
1300
+ for (const key in schemas) {
1301
+ traverse(schemasLocation.getPropertyLocation(key), nestedDepth)
1302
+ }
1045
1303
  }
1046
1304
  }
1047
- if (schema.allOf) {
1048
- const allOfLocation = location.getPropertyLocation('allOf')
1049
- for (let i = 0; i < schema.allOf.length; i++) {
1050
- traverse(allOfLocation.getPropertyLocation(i))
1305
+
1306
+ const dependencies = schema.dependencies
1307
+ if (typeof dependencies === 'object' && dependencies !== null) {
1308
+ const dependenciesLocation = location.getPropertyLocation('dependencies')
1309
+ for (const key in dependencies) {
1310
+ if (!Array.isArray(dependencies[key])) {
1311
+ traverse(dependenciesLocation.getPropertyLocation(key), nestedDepth)
1312
+ }
1051
1313
  }
1052
1314
  }
1053
- if (schema.then) traverse(location.getPropertyLocation('then'))
1054
- if (schema.else) traverse(location.getPropertyLocation('else'))
1055
1315
 
1056
1316
  pathStack.delete(fullPath)
1057
1317
  }
1058
- traverse(location)
1318
+ traverse(location, 0)
1059
1319
  }
1060
1320
 
1061
1321
  function buildConstSerializer (location, input) {
@@ -1074,7 +1334,7 @@ function buildConstSerializer (location, input) {
1074
1334
  `
1075
1335
  }
1076
1336
 
1077
- code += `json += '${JSON.stringify(schema.const).replace(SINGLE_TICK, "\\'")}'`
1337
+ code += `json += ${JSON.stringify(JSON.stringify(schema.const))}`
1078
1338
 
1079
1339
  if (hasNullType) {
1080
1340
  code += `
@@ -1151,17 +1411,18 @@ function buildOneOf (context, location, input) {
1151
1411
  }
1152
1412
 
1153
1413
  const nestedResult = buildValue(context, mergedLocation, input)
1154
- const schemaRef = optionLocation.getSchemaRef()
1414
+ const schemaRef = getValidatorSchemaRef(context, optionLocation)
1415
+ context.validatorSchemaRefs.add(schemaRef)
1155
1416
 
1156
1417
  code += `
1157
- ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) {
1418
+ ${index === 0 ? 'if' : 'else if'}(validator.validate(${JSON.stringify(schemaRef)}, ${input})) {
1158
1419
  ${nestedResult}
1159
1420
  }
1160
1421
  `
1161
1422
  }
1162
1423
 
1163
1424
  code += `
1164
- else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
1425
+ else throw new TypeError(${JSON.stringify(`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.`)})
1165
1426
  `
1166
1427
 
1167
1428
  return code
@@ -1184,26 +1445,31 @@ function buildIfThenElse (context, location, input) {
1184
1445
  )
1185
1446
 
1186
1447
  const ifLocation = location.getPropertyLocation('if')
1187
- const ifSchemaRef = ifLocation.getSchemaRef()
1188
-
1189
- const thenLocation = location.getPropertyLocation('then')
1190
- let thenMergedSchemaId = context.mergedSchemasIds.get(thenSchema)
1191
- let thenMergedLocation = null
1192
- if (thenMergedSchemaId) {
1193
- thenMergedLocation = getMergedLocation(context, thenMergedSchemaId)
1194
- } else {
1195
- thenMergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
1196
- context.mergedSchemasIds.set(thenSchema, thenMergedSchemaId)
1448
+ const ifSchemaRef = getValidatorSchemaRef(context, ifLocation)
1449
+ context.validatorSchemaRefs.add(ifSchemaRef)
1450
+
1451
+ // `then` is optional: a schema may pair `if` with `else` alone. In that case
1452
+ // the true branch adds no keywords, so it serializes with the root schema.
1453
+ let thenMergedLocation = rootLocation
1454
+ if (thenSchema !== undefined) {
1455
+ const thenLocation = location.getPropertyLocation('then')
1456
+ let thenMergedSchemaId = context.mergedSchemasIds.get(thenSchema)
1457
+ if (thenMergedSchemaId) {
1458
+ thenMergedLocation = getMergedLocation(context, thenMergedSchemaId)
1459
+ } else {
1460
+ thenMergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
1461
+ context.mergedSchemasIds.set(thenSchema, thenMergedSchemaId)
1197
1462
 
1198
- thenMergedLocation = mergeLocations(context, thenMergedSchemaId, [
1199
- rootLocation,
1200
- thenLocation
1201
- ])
1463
+ thenMergedLocation = mergeLocations(context, thenMergedSchemaId, [
1464
+ rootLocation,
1465
+ thenLocation
1466
+ ])
1467
+ }
1202
1468
  }
1203
1469
 
1204
1470
  if (!elseSchema) {
1205
1471
  return `
1206
- if (validator.validate("${ifSchemaRef}", ${input})) {
1472
+ if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
1207
1473
  ${buildValue(context, thenMergedLocation, input)}
1208
1474
  } else {
1209
1475
  ${buildValue(context, rootLocation, input)}
@@ -1227,7 +1493,7 @@ function buildIfThenElse (context, location, input) {
1227
1493
  }
1228
1494
 
1229
1495
  return `
1230
- if (validator.validate("${ifSchemaRef}", ${input})) {
1496
+ if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
1231
1497
  ${buildValue(context, thenMergedLocation, input)}
1232
1498
  } else {
1233
1499
  ${buildValue(context, elseMergedLocation, input)}
@@ -1255,7 +1521,7 @@ function buildValue (context, location, input) {
1255
1521
  return buildOneOf(context, location, input)
1256
1522
  }
1257
1523
 
1258
- if (schema.if && schema.then) {
1524
+ if (schema.if && (schema.then || schema.else)) {
1259
1525
  return buildIfThenElse(context, location, input)
1260
1526
  }
1261
1527
 
package/lib/location.js CHANGED
@@ -8,10 +8,12 @@ class Location {
8
8
  }
9
9
 
10
10
  getPropertyLocation (propertyName) {
11
+ // a JSON pointer segment, so a property name with a / or a ~ still points at the property
12
+ const segment = String(propertyName).replace(/~/g, '~0').replace(/\//g, '~1')
11
13
  const propertyLocation = new Location(
12
14
  this.schema[propertyName],
13
15
  this.schemaId,
14
- this.jsonPointer + '/' + propertyName
16
+ this.jsonPointer + '/' + segment
15
17
  )
16
18
  return propertyLocation
17
19
  }