fast-json-stringify 5.14.0 → 5.15.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
@@ -140,7 +140,19 @@ function build (schema, options) {
140
140
  const location = new Location(schema, context.rootSchemaId)
141
141
  const code = buildValue(context, location, 'input')
142
142
 
143
- let contextFunctionCode
143
+ let contextFunctionCode = `
144
+ const JSON_STR_BEGIN_OBJECT = '{'
145
+ const JSON_STR_END_OBJECT = '}'
146
+ const JSON_STR_BEGIN_ARRAY = '['
147
+ const JSON_STR_END_ARRAY = ']'
148
+ const JSON_STR_COMMA = ','
149
+ const JSON_STR_COLONS = ':'
150
+ const JSON_STR_QUOTE = '"'
151
+ const JSON_STR_EMPTY_OBJECT = JSON_STR_BEGIN_OBJECT + JSON_STR_END_OBJECT
152
+ const JSON_STR_EMPTY_ARRAY = JSON_STR_BEGIN_ARRAY + JSON_STR_END_ARRAY
153
+ const JSON_STR_EMPTY_STRING = JSON_STR_QUOTE + JSON_STR_QUOTE
154
+ const JSON_STR_NULL = 'null'
155
+ `
144
156
 
145
157
  // If we have only the invocation of the 'anonymous0' function, we would
146
158
  // basically just wrap the 'anonymous0' function in the 'main' function and
@@ -148,13 +160,13 @@ function build (schema, options) {
148
160
  // wrapping and the unnecessary memory allocation by aliasing 'anonymous0' to
149
161
  // 'main'
150
162
  if (code === 'json += anonymous0(input)') {
151
- contextFunctionCode = `
163
+ contextFunctionCode += `
152
164
  ${context.functions.join('\n')}
153
165
  const main = anonymous0
154
166
  return main
155
167
  `
156
168
  } else {
157
- contextFunctionCode = `
169
+ contextFunctionCode += `
158
170
  function main (input) {
159
171
  let json = ''
160
172
  ${code}
@@ -284,7 +296,7 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma) {
284
296
  code += `
285
297
  if (/${propertyKey.replace(/\\*\//g, '\\/')}/.test(key)) {
286
298
  ${addComma}
287
- json += serializer.asString(key) + ':'
299
+ json += serializer.asString(key) + JSON_STR_COLONS
288
300
  ${buildValue(context, propertyLocation, 'value')}
289
301
  continue
290
302
  }
@@ -299,13 +311,13 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma) {
299
311
  if (additionalPropertiesSchema === true) {
300
312
  code += `
301
313
  ${addComma}
302
- json += serializer.asString(key) + ':' + JSON.stringify(value)
314
+ json += serializer.asString(key) + JSON_STR_COLONS + JSON.stringify(value)
303
315
  `
304
316
  } else {
305
317
  const propertyLocation = location.getPropertyLocation('additionalProperties')
306
318
  code += `
307
319
  ${addComma}
308
- json += serializer.asString(key) + ':'
320
+ json += serializer.asString(key) + JSON_STR_COLONS
309
321
  ${buildValue(context, propertyLocation, 'value')}
310
322
  `
311
323
  }
@@ -333,7 +345,7 @@ function buildInnerObject (context, location) {
333
345
  )
334
346
  const hasRequiredProperties = requiredProperties.includes(propertiesKeys[0])
335
347
 
336
- let code = ''
348
+ let code = 'let value\n'
337
349
 
338
350
  for (const key of requiredProperties) {
339
351
  if (!propertiesKeys.includes(key)) {
@@ -341,12 +353,12 @@ function buildInnerObject (context, location) {
341
353
  }
342
354
  }
343
355
 
344
- code += 'let json = \'{\'\n'
356
+ code += 'let json = JSON_STR_BEGIN_OBJECT\n'
345
357
 
346
358
  let addComma = ''
347
359
  if (!hasRequiredProperties) {
348
360
  code += 'let addComma = false\n'
349
- addComma = '!addComma && (addComma = true) || (json += \',\')'
361
+ addComma = '!addComma && (addComma = true) || (json += JSON_STR_COMMA)'
350
362
  }
351
363
 
352
364
  for (const key of propertiesKeys) {
@@ -360,10 +372,11 @@ function buildInnerObject (context, location) {
360
372
  const isRequired = requiredProperties.includes(key)
361
373
 
362
374
  code += `
363
- if (obj[${sanitizedKey}] !== undefined) {
375
+ value = obj[${sanitizedKey}]
376
+ if (value !== undefined) {
364
377
  ${addComma}
365
378
  json += ${JSON.stringify(sanitizedKey + ':')}
366
- ${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
379
+ ${buildValue(context, propertyLocation, 'value')}
367
380
  }`
368
381
 
369
382
  if (defaultValue !== undefined) {
@@ -391,7 +404,7 @@ function buildInnerObject (context, location) {
391
404
  }
392
405
 
393
406
  code += `
394
- return json + '}'
407
+ return json + JSON_STR_END_OBJECT
395
408
  `
396
409
  return code
397
410
  }
@@ -438,7 +451,7 @@ function cloneOriginSchema (context, schema, schemaId) {
438
451
  for (const key in schema) {
439
452
  let value = schema[key]
440
453
 
441
- if (key === '$ref' && value.charAt(0) === '#') {
454
+ if (key === '$ref' && typeof value === 'string' && value.charAt(0) === '#') {
442
455
  value = schemaId + value
443
456
  }
444
457
 
@@ -482,7 +495,7 @@ function buildObject (context, location) {
482
495
  // ${schemaRef}
483
496
  function ${functionName} (input) {
484
497
  const obj = ${toJSON('input')}
485
- ${!nullable ? 'if (obj === null) return \'{}\'' : ''}
498
+ ${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_OBJECT' : ''}
486
499
 
487
500
  ${buildInnerObject(context, location)}
488
501
  }
@@ -523,7 +536,7 @@ function buildArray (context, location) {
523
536
 
524
537
  const nullable = schema.nullable === true
525
538
  functionCode += `
526
- ${!nullable ? 'if (obj === null) return \'[]\'' : ''}
539
+ ${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_ARRAY' : ''}
527
540
  if (!Array.isArray(obj)) {
528
541
  throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
529
542
  }
@@ -539,25 +552,26 @@ function buildArray (context, location) {
539
552
  }
540
553
 
541
554
  if (largeArrayMechanism === 'json-stringify') {
542
- functionCode += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
555
+ functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
543
556
  }
544
557
 
545
558
  functionCode += `
546
- let jsonOutput = ''
559
+ const arrayEnd = arrayLength - 1
560
+ let value
561
+ let json = ''
547
562
  `
548
563
 
549
564
  if (Array.isArray(itemsSchema)) {
550
565
  for (let i = 0; i < itemsSchema.length; i++) {
551
566
  const item = itemsSchema[i]
552
- const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), `obj[${i}]`)
567
+ functionCode += `value = obj[${i}]`
568
+ const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), 'value')
553
569
  functionCode += `
554
570
  if (${i} < arrayLength) {
555
571
  if (${buildArrayTypeCondition(item.type, `[${i}]`)}) {
556
- let json = ''
557
572
  ${tmpRes}
558
- jsonOutput += json
559
- if (${i} < arrayLength - 1) {
560
- jsonOutput += ','
573
+ if (${i} < arrayEnd) {
574
+ json += JSON_STR_COMMA
561
575
  }
562
576
  } else {
563
577
  throw new Error(\`Item at ${i} does not match schema definition.\`)
@@ -569,9 +583,9 @@ function buildArray (context, location) {
569
583
  if (schema.additionalItems) {
570
584
  functionCode += `
571
585
  for (let i = ${itemsSchema.length}; i < arrayLength; i++) {
572
- jsonOutput += JSON.stringify(obj[i])
573
- if (i < arrayLength - 1) {
574
- jsonOutput += ','
586
+ json += JSON.stringify(obj[i])
587
+ if (i < arrayEnd) {
588
+ json += JSON_STR_COMMA
575
589
  }
576
590
  }`
577
591
  }
@@ -579,17 +593,15 @@ function buildArray (context, location) {
579
593
  const code = buildValue(context, itemsLocation, 'obj[i]')
580
594
  functionCode += `
581
595
  for (let i = 0; i < arrayLength; i++) {
582
- let json = ''
583
596
  ${code}
584
- jsonOutput += json
585
- if (i < arrayLength - 1) {
586
- jsonOutput += ','
597
+ if (i < arrayEnd) {
598
+ json += JSON_STR_COMMA
587
599
  }
588
600
  }`
589
601
  }
590
602
 
591
603
  functionCode += `
592
- return \`[\${jsonOutput}]\`
604
+ return JSON_STR_BEGIN_ARRAY + json + JSON_STR_END_ARRAY
593
605
  }`
594
606
 
595
607
  context.functions.push(functionCode)
@@ -600,33 +612,33 @@ function buildArrayTypeCondition (type, accessor) {
600
612
  let condition
601
613
  switch (type) {
602
614
  case 'null':
603
- condition = `obj${accessor} === null`
615
+ condition = 'value === null'
604
616
  break
605
617
  case 'string':
606
- condition = `typeof obj${accessor} === 'string' ||
607
- obj${accessor} === null ||
608
- obj${accessor} instanceof Date ||
609
- obj${accessor} instanceof RegExp ||
618
+ condition = `typeof value === 'string' ||
619
+ value === null ||
620
+ value instanceof Date ||
621
+ value instanceof RegExp ||
610
622
  (
611
- typeof obj${accessor} === "object" &&
612
- typeof obj${accessor}.toString === "function" &&
613
- obj${accessor}.toString !== Object.prototype.toString
623
+ typeof value === "object" &&
624
+ typeof value.toString === "function" &&
625
+ value.toString !== Object.prototype.toString
614
626
  )`
615
627
  break
616
628
  case 'integer':
617
- condition = `Number.isInteger(obj${accessor})`
629
+ condition = 'Number.isInteger(value)'
618
630
  break
619
631
  case 'number':
620
- condition = `Number.isFinite(obj${accessor})`
632
+ condition = 'Number.isFinite(value)'
621
633
  break
622
634
  case 'boolean':
623
- condition = `typeof obj${accessor} === 'boolean'`
635
+ condition = 'typeof value === \'boolean\''
624
636
  break
625
637
  case 'object':
626
- condition = `obj${accessor} && typeof obj${accessor} === 'object' && obj${accessor}.constructor === Object`
638
+ condition = 'value && typeof value === \'object\' && value.constructor === Object'
627
639
  break
628
640
  case 'array':
629
- condition = `Array.isArray(obj${accessor})`
641
+ condition = 'Array.isArray(value)'
630
642
  break
631
643
  default:
632
644
  if (Array.isArray(type)) {
@@ -717,7 +729,7 @@ function buildSingleTypeSerializer (context, location, input) {
717
729
 
718
730
  switch (schema.type) {
719
731
  case 'null':
720
- return 'json += \'null\''
732
+ return 'json += JSON_STR_NULL'
721
733
  case 'string': {
722
734
  if (schema.format === 'date-time') {
723
735
  return `json += serializer.asDateTime(${input})`
@@ -731,9 +743,9 @@ function buildSingleTypeSerializer (context, location, input) {
731
743
  return `
732
744
  if (typeof ${input} !== 'string') {
733
745
  if (${input} === null) {
734
- json += '""'
746
+ json += JSON_STR_EMPTY_STRING
735
747
  } else if (${input} instanceof Date) {
736
- json += '"' + ${input}.toISOString() + '"'
748
+ json += JSON_STR_QUOTE + ${input}.toISOString() + JSON_STR_QUOTE
737
749
  } else if (${input} instanceof RegExp) {
738
750
  json += serializer.asString(${input}.source)
739
751
  } else {
@@ -777,7 +789,7 @@ function buildConstSerializer (location, input) {
777
789
  if (hasNullType) {
778
790
  code += `
779
791
  if (${input} === null) {
780
- json += 'null'
792
+ json += JSON_STR_NULL
781
793
  } else {
782
794
  `
783
795
  }
@@ -984,7 +996,7 @@ function buildValue (context, location, input) {
984
996
  if (nullable) {
985
997
  code += `
986
998
  if (${input} === null) {
987
- json += 'null'
999
+ json += JSON_STR_NULL
988
1000
  } else {
989
1001
  `
990
1002
  }
package/lib/serializer.js CHANGED
@@ -24,38 +24,29 @@ module.exports = class Serializer {
24
24
  }
25
25
 
26
26
  asInteger (i) {
27
- if (typeof i === 'number') {
28
- if (Number.isInteger(i)) {
29
- return '' + i
30
- }
31
- // check if number is Infinity or NaN
32
- // eslint-disable-next-line no-self-compare
33
- if (i === Infinity || i === -Infinity || i !== i) {
34
- throw new Error(`The value "${i}" cannot be converted to an integer.`)
35
- }
36
- return this.parseInteger(i)
37
- } else if (i === null) {
38
- return '0'
27
+ if (Number.isInteger(i)) {
28
+ return '' + i
39
29
  } else if (typeof i === 'bigint') {
40
30
  return i.toString()
41
- } else {
42
- /* eslint no-undef: "off" */
43
- const integer = this.parseInteger(i)
44
- if (Number.isFinite(integer)) {
45
- return '' + integer
46
- } else {
47
- throw new Error(`The value "${i}" cannot be converted to an integer.`)
48
- }
49
31
  }
32
+ /* eslint no-undef: "off" */
33
+ const integer = this.parseInteger(i)
34
+ // check if number is Infinity or NaN
35
+ // eslint-disable-next-line no-self-compare
36
+ if (integer === Infinity || integer === -Infinity || integer !== integer) {
37
+ throw new Error(`The value "${i}" cannot be converted to an integer.`)
38
+ }
39
+ return '' + integer
50
40
  }
51
41
 
52
42
  asNumber (i) {
53
- const num = Number(i)
43
+ // fast cast to number
44
+ const num = +i
54
45
  // check if number is NaN
55
46
  // eslint-disable-next-line no-self-compare
56
47
  if (num !== num) {
57
48
  throw new Error(`The value "${i}" cannot be converted to a number.`)
58
- } else if (!Number.isFinite(num)) {
49
+ } else if (num === Infinity || num === -Infinity) {
59
50
  return 'null'
60
51
  } else {
61
52
  return '' + num
@@ -100,9 +91,34 @@ module.exports = class Serializer {
100
91
  }
101
92
 
102
93
  asString (str) {
103
- if (str.length < 42) {
104
- return this.asStringSmall(str)
105
- } else if (str.length < 5000 && STR_ESCAPE.test(str) === false) {
94
+ const len = str.length
95
+ if (len < 42) {
96
+ // magically escape strings for json
97
+ // relying on their charCodeAt
98
+ // everything below 32 needs JSON.stringify()
99
+ // every string that contain surrogate needs JSON.stringify()
100
+ // 34 and 92 happens all the time, so we
101
+ // have a fast case for them
102
+ let result = ''
103
+ let last = -1
104
+ let point = 255
105
+ // eslint-disable-next-line
106
+ for (var i = 0; i < len; i++) {
107
+ point = str.charCodeAt(i)
108
+ if (
109
+ point === 0x22 || // '"'
110
+ point === 0x5c // '\'
111
+ ) {
112
+ last === -1 && (last = 0)
113
+ result += str.slice(last, i) + '\\'
114
+ last = i
115
+ } else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
116
+ // The current character is non-printable characters or a surrogate.
117
+ return JSON.stringify(str)
118
+ }
119
+ }
120
+ return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
121
+ } else if (len < 5000 && STR_ESCAPE.test(str) === false) {
106
122
  // Only use the regular expression for shorter input. The overhead is otherwise too much.
107
123
  return '"' + str + '"'
108
124
  } else {
@@ -114,37 +130,6 @@ module.exports = class Serializer {
114
130
  return '"' + str + '"'
115
131
  }
116
132
 
117
- // magically escape strings for json
118
- // relying on their charCodeAt
119
- // everything below 32 needs JSON.stringify()
120
- // every string that contain surrogate needs JSON.stringify()
121
- // 34 and 92 happens all the time, so we
122
- // have a fast case for them
123
- asStringSmall (str) {
124
- const len = str.length
125
- let result = ''
126
- let last = -1
127
- let point = 255
128
-
129
- // eslint-disable-next-line
130
- for (var i = 0; i < len; i++) {
131
- point = str.charCodeAt(i)
132
- if (
133
- point === 0x22 || // '"'
134
- point === 0x5c // '\'
135
- ) {
136
- last === -1 && (last = 0)
137
- result += str.slice(last, i) + '\\'
138
- last = i
139
- } else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
140
- // The current character is non-printable characters or a surrogate.
141
- return JSON.stringify(str)
142
- }
143
- }
144
-
145
- return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
146
- }
147
-
148
133
  getState () {
149
134
  return this._options
150
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.14.0",
3
+ "version": "5.15.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -717,3 +717,39 @@ test('external recursive allOfs', (t) => {
717
717
  const stringify = build(schema, { schema: { externalSchema } })
718
718
  t.equal(stringify(data), '{"a":{"bar":"42","foo":{}},"b":{"bar":"42","foo":{}}}')
719
719
  })
720
+
721
+ test('do not crash with $ref prop', (t) => {
722
+ t.plan(1)
723
+
724
+ const schema = {
725
+ title: 'object with $ref',
726
+ type: 'object',
727
+ properties: {
728
+ outside: {
729
+ $ref: '#/$defs/outside'
730
+ }
731
+ },
732
+ $defs: {
733
+ inside: {
734
+ type: 'object',
735
+ properties: {
736
+ $ref: {
737
+ type: 'string'
738
+ }
739
+ }
740
+ },
741
+ outside: {
742
+ allOf: [{
743
+ $ref: '#/$defs/inside'
744
+ }]
745
+ }
746
+ }
747
+ }
748
+ const stringify = build(schema)
749
+ const value = stringify({
750
+ outside: {
751
+ $ref: 'true'
752
+ }
753
+ })
754
+ t.equal(value, '{"outside":{"$ref":"true"}}')
755
+ })