fast-json-stringify 7.0.1 → 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.
package/index.js CHANGED
@@ -13,6 +13,32 @@ const mergeSchemas = require('./lib/merge-schemas')
13
13
  let largeArraySize = 2e4
14
14
  let largeArrayMechanism = 'default'
15
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
+
16
42
  const serializerFns = `
17
43
  const {
18
44
  asString,
@@ -42,6 +68,113 @@ const validLargeArrayMechanisms = new Set([
42
68
 
43
69
  let schemaIdCounter = 0
44
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
+
45
178
  function isValidSchema (schema, name) {
46
179
  if (!validate(schema)) {
47
180
  if (name) {
@@ -57,27 +190,64 @@ function isValidSchema (schema, name) {
57
190
  }
58
191
 
59
192
  function resolveRef (context, location) {
60
- const ref = location.schema.$ref
193
+ const seen = new Set()
194
+ let depth = 0
61
195
 
62
- let hashIndex = ref.indexOf('#')
63
- if (hashIndex === -1) {
64
- hashIndex = ref.length
65
- }
196
+ while (location.schema.$ref !== undefined) {
197
+ const ref = location.schema.$ref
198
+ const locationRef = location.getSchemaRef()
66
199
 
67
- const schemaId = ref.slice(0, hashIndex) || location.schemaId
68
- 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
+ }
69
221
 
70
- const schema = context.refResolver.getSchema(schemaId, jsonPointer)
71
- if (schema === null) {
72
- throw new Error(`Cannot find reference "${ref}"`)
222
+ location = new Location(schema, schemaId, jsonPointer)
73
223
  }
74
224
 
75
- const newLocation = new Location(schema, schemaId, jsonPointer)
76
- if (schema.$ref !== undefined) {
77
- 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
+ }
78
248
  }
79
249
 
80
- return newLocation
250
+ return dependencies
81
251
  }
82
252
 
83
253
  function getMergedLocation (context, mergedSchemaId) {
@@ -92,6 +262,14 @@ function getSchemaId (schema, rootSchemaId) {
92
262
  return rootSchemaId
93
263
  }
94
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
+
95
273
  function getSafeSchemaRef (context, location) {
96
274
  let schemaRef = location.getSchemaRef() || ''
97
275
  if (schemaRef.startsWith(context.rootSchemaId)) {
@@ -100,11 +278,59 @@ function getSafeSchemaRef (context, location) {
100
278
  return schemaRef
101
279
  }
102
280
 
103
- function build (schema, options) {
104
- isValidSchema(schema)
281
+ function getValidatorSchemaRef (context, location) {
282
+ let schemaRef = location.getSchemaRef()
283
+ const schema = location.schema
105
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
+ }
322
+
323
+ return schemaRef
324
+ }
325
+
326
+ function build (schema, options) {
106
327
  options = options || {}
107
328
 
329
+ const maxDepth = getMaxDepth(options)
330
+ assertObjectDepth(schema, maxDepth)
331
+ assertSchemaDepth(schema, maxDepth)
332
+ isValidSchema(schema)
333
+
108
334
  const context = {
109
335
  functions: [],
110
336
  functionsCounter: 0,
@@ -113,7 +339,9 @@ function build (schema, options) {
113
339
  refResolver: new RefResolver(),
114
340
  rootSchemaId: schema.$id || `__fjs_root_${schemaIdCounter++}`,
115
341
  validatorSchemasIds: new Set(),
342
+ validatorSchemaRefs: new Set(),
116
343
  mergedSchemasIds: new Map(),
344
+ maxDepth,
117
345
  recursiveSchemas: new Set(),
118
346
  recursivePaths: new Set(),
119
347
  buildingSet: new Set(),
@@ -130,6 +358,8 @@ function build (schema, options) {
130
358
  const schema = options.schema[key]
131
359
  const schemaId = getSchemaId(schema, key)
132
360
  if (!context.refResolver.hasSchema(schemaId)) {
361
+ assertObjectDepth(schema, maxDepth, key)
362
+ assertSchemaDepth(schema, maxDepth, key)
133
363
  isValidSchema(schema, key)
134
364
  context.refResolver.addSchema(schema, key)
135
365
  }
@@ -208,18 +438,25 @@ function build (schema, options) {
208
438
  }
209
439
 
210
440
  const serializer = new Serializer(options)
211
- const validator = new Validator(options.ajv)
441
+ const validator = new Validator(
442
+ options.ajv,
443
+ options.mode === 'standalone' && options.inlineValidators
444
+ )
212
445
 
213
446
  for (const schemaId of context.validatorSchemasIds) {
214
447
  const schema = context.refResolver.getSchema(schemaId)
215
448
  validator.addSchema(schema, schemaId)
216
449
 
217
- const dependencies = context.refResolver.getSchemaDependencies(schemaId)
450
+ const dependencies = getSchemaDependencies(context.refResolver, schemaId)
218
451
  for (const [schemaId, schema] of Object.entries(dependencies)) {
219
452
  validator.addSchema(schema, schemaId)
220
453
  }
221
454
  }
222
455
 
456
+ if (options.compileValidators) {
457
+ validator.compileSchemas(context.validatorSchemaRefs)
458
+ }
459
+
223
460
  if (options.mode === 'debug') {
224
461
  return {
225
462
  validator,
@@ -329,7 +566,12 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma, objV
329
566
  const additionalPropertiesLocation = location.getPropertyLocation('additionalProperties')
330
567
  const additionalPropertiesSchema = additionalPropertiesLocation.schema
331
568
 
332
- 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) {
333
575
  if (additionalPropertiesSchema === true) {
334
576
  code += `
335
577
  ${addComma}
@@ -447,12 +689,17 @@ function buildInnerObject (context, location, objVar) {
447
689
  const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
448
690
  const defaultValue = propertyLocation.schema.default
449
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)}`
450
698
 
451
699
  code += `
452
700
  const ${value} = ${objVar}[${sanitizedKey}]
453
701
  if (${value} !== undefined) {
454
- ${addComma}
455
- json += ${JSON.stringify(sanitizedKey + ':')}
702
+ ${addProperty}
456
703
  ${buildValue(context, propertyLocation, `${value}`)}
457
704
  }`
458
705
 
@@ -568,7 +815,7 @@ function buildObject (context, location, input) {
568
815
  const schemaRef = getSafeSchemaRef(context, location)
569
816
 
570
817
  const functionCode = `
571
- // ${schemaRef}
818
+ // ${asComment(schemaRef)}
572
819
  function ${functionName} (input) {
573
820
  const obj = ${toJSON('input')}
574
821
  if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
@@ -628,14 +875,14 @@ function buildArray (context, location, input) {
628
875
 
629
876
  let functionCode = `
630
877
  function ${functionName} (obj) {
631
- // ${schemaRef}
878
+ // ${asComment(schemaRef)}
632
879
  let json = ''
633
880
  `
634
881
 
635
882
  functionCode += `
636
883
  if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
637
884
  if (!Array.isArray(obj)) {
638
- 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.`)})
639
886
  }
640
887
  const arrayLength = obj.length
641
888
  `
@@ -720,7 +967,7 @@ function buildArray (context, location, input) {
720
967
  if (${objVar} === null) {
721
968
  json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
722
969
  } else if (!Array.isArray(${objVar})) {
723
- 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.`)})
724
971
  } else {
725
972
  const arrayLength_${objVar} = ${objVar}.length
726
973
  `
@@ -900,6 +1147,17 @@ function buildMultiTypeSerializer (context, location, input) {
900
1147
  `
901
1148
  break
902
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
+ }
903
1161
  default: {
904
1162
  code += `
905
1163
  ${statement}(typeof ${input} === "${type}" || ${input} === null) {
@@ -911,7 +1169,7 @@ function buildMultiTypeSerializer (context, location, input) {
911
1169
  }
912
1170
  })
913
1171
  code += `
914
- 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.`)})
915
1173
  `
916
1174
 
917
1175
  return code
@@ -971,9 +1229,13 @@ function buildSingleTypeSerializer (context, location, input) {
971
1229
 
972
1230
  function detectRecursiveSchemas (context, location) {
973
1231
  const pathStack = new Set()
974
- function traverse (location) {
1232
+ function traverse (location, depth) {
975
1233
  const schema = location.schema
976
- 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
+ }
977
1239
 
978
1240
  const schemaId = location.schemaId || ''
979
1241
  const jsonPointer = location.jsonPointer || ''
@@ -994,65 +1256,66 @@ function detectRecursiveSchemas (context, location) {
994
1256
  if (schema.$ref) {
995
1257
  try {
996
1258
  const res = resolveRef(context, location)
997
- traverse(res)
1259
+ traverse(res, depth)
998
1260
  } catch (err) {
1261
+ if (!err.message.startsWith('Cannot find reference')) throw err
999
1262
  // Validation will handle missing refs later
1000
1263
  }
1001
1264
  }
1002
1265
 
1003
- if (schema.properties) {
1004
- const propertiesLocation = location.getPropertyLocation('properties')
1005
- for (const key in schema.properties) {
1006
- traverse(propertiesLocation.getPropertyLocation(key))
1007
- }
1008
- }
1009
- if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
1010
- traverse(location.getPropertyLocation('additionalProperties'))
1011
- }
1012
- if (schema.patternProperties) {
1013
- const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
1014
- for (const key in schema.patternProperties) {
1015
- 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)
1016
1271
  }
1017
1272
  }
1018
- if (schema.items) {
1019
- const itemsLocation = location.getPropertyLocation('items')
1020
- if (Array.isArray(schema.items)) {
1021
- for (let i = 0; i < schema.items.length; i++) {
1022
- 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)
1023
1281
  }
1024
- } else {
1025
- traverse(itemsLocation)
1026
1282
  }
1027
1283
  }
1028
- if (schema.additionalItems && typeof schema.additionalItems === 'object') {
1029
- traverse(location.getPropertyLocation('additionalItems'))
1030
- }
1031
1284
 
1032
- if (schema.oneOf) {
1033
- const oneOfLocation = location.getPropertyLocation('oneOf')
1034
- for (let i = 0; i < schema.oneOf.length; i++) {
1035
- 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)
1036
1291
  }
1292
+ } else if (typeof items === 'object' && items !== null) {
1293
+ traverse(location.getPropertyLocation('items'), nestedDepth)
1037
1294
  }
1038
- if (schema.anyOf) {
1039
- const anyOfLocation = location.getPropertyLocation('anyOf')
1040
- for (let i = 0; i < schema.anyOf.length; i++) {
1041
- 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
+ }
1042
1303
  }
1043
1304
  }
1044
- if (schema.allOf) {
1045
- const allOfLocation = location.getPropertyLocation('allOf')
1046
- for (let i = 0; i < schema.allOf.length; i++) {
1047
- 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
+ }
1048
1313
  }
1049
1314
  }
1050
- if (schema.then) traverse(location.getPropertyLocation('then'))
1051
- if (schema.else) traverse(location.getPropertyLocation('else'))
1052
1315
 
1053
1316
  pathStack.delete(fullPath)
1054
1317
  }
1055
- traverse(location)
1318
+ traverse(location, 0)
1056
1319
  }
1057
1320
 
1058
1321
  function buildConstSerializer (location, input) {
@@ -1148,17 +1411,18 @@ function buildOneOf (context, location, input) {
1148
1411
  }
1149
1412
 
1150
1413
  const nestedResult = buildValue(context, mergedLocation, input)
1151
- const schemaRef = optionLocation.getSchemaRef()
1414
+ const schemaRef = getValidatorSchemaRef(context, optionLocation)
1415
+ context.validatorSchemaRefs.add(schemaRef)
1152
1416
 
1153
1417
  code += `
1154
- ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) {
1418
+ ${index === 0 ? 'if' : 'else if'}(validator.validate(${JSON.stringify(schemaRef)}, ${input})) {
1155
1419
  ${nestedResult}
1156
1420
  }
1157
1421
  `
1158
1422
  }
1159
1423
 
1160
1424
  code += `
1161
- 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.`)})
1162
1426
  `
1163
1427
 
1164
1428
  return code
@@ -1181,26 +1445,31 @@ function buildIfThenElse (context, location, input) {
1181
1445
  )
1182
1446
 
1183
1447
  const ifLocation = location.getPropertyLocation('if')
1184
- const ifSchemaRef = ifLocation.getSchemaRef()
1185
-
1186
- const thenLocation = location.getPropertyLocation('then')
1187
- let thenMergedSchemaId = context.mergedSchemasIds.get(thenSchema)
1188
- let thenMergedLocation = null
1189
- if (thenMergedSchemaId) {
1190
- thenMergedLocation = getMergedLocation(context, thenMergedSchemaId)
1191
- } else {
1192
- thenMergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
1193
- 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)
1194
1462
 
1195
- thenMergedLocation = mergeLocations(context, thenMergedSchemaId, [
1196
- rootLocation,
1197
- thenLocation
1198
- ])
1463
+ thenMergedLocation = mergeLocations(context, thenMergedSchemaId, [
1464
+ rootLocation,
1465
+ thenLocation
1466
+ ])
1467
+ }
1199
1468
  }
1200
1469
 
1201
1470
  if (!elseSchema) {
1202
1471
  return `
1203
- if (validator.validate("${ifSchemaRef}", ${input})) {
1472
+ if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
1204
1473
  ${buildValue(context, thenMergedLocation, input)}
1205
1474
  } else {
1206
1475
  ${buildValue(context, rootLocation, input)}
@@ -1224,7 +1493,7 @@ function buildIfThenElse (context, location, input) {
1224
1493
  }
1225
1494
 
1226
1495
  return `
1227
- if (validator.validate("${ifSchemaRef}", ${input})) {
1496
+ if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
1228
1497
  ${buildValue(context, thenMergedLocation, input)}
1229
1498
  } else {
1230
1499
  ${buildValue(context, elseMergedLocation, input)}
@@ -1252,7 +1521,7 @@ function buildValue (context, location, input) {
1252
1521
  return buildOneOf(context, location, input)
1253
1522
  }
1254
1523
 
1255
- if (schema.if && schema.then) {
1524
+ if (schema.if && (schema.then || schema.else)) {
1256
1525
  return buildIfThenElse(context, location, input)
1257
1526
  }
1258
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
  }