fast-json-stringify 5.11.1 → 5.13.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/lib/location.js CHANGED
@@ -5,7 +5,6 @@ class Location {
5
5
  this.schema = schema
6
6
  this.schemaId = schemaId
7
7
  this.jsonPointer = jsonPointer
8
- this.mergedSchemaId = null
9
8
  }
10
9
 
11
10
  getPropertyLocation (propertyName) {
@@ -14,39 +13,11 @@ class Location {
14
13
  this.schemaId,
15
14
  this.jsonPointer + '/' + propertyName
16
15
  )
17
-
18
- if (this.mergedSchemaId !== null) {
19
- propertyLocation.addMergedSchema(
20
- this.schema[propertyName],
21
- this.mergedSchemaId,
22
- this.jsonPointer + '/' + propertyName
23
- )
24
- }
25
-
26
16
  return propertyLocation
27
17
  }
28
18
 
29
- // Use this method to get current schema location.
30
- // Use it when you need to create reference to the current location.
31
- getSchemaId () {
32
- return this.mergedSchemaId || this.schemaId
33
- }
34
-
35
- // Use this method to get original schema id for resolving user schema $refs
36
- // Don't join it with a JSON pointer to get the current location.
37
- getOriginSchemaId () {
38
- return this.schemaId
39
- }
40
-
41
19
  getSchemaRef () {
42
- const schemaId = this.getSchemaId()
43
- return schemaId + this.jsonPointer
44
- }
45
-
46
- addMergedSchema (mergedSchema, schemaId, jsonPointer = '#') {
47
- this.schema = mergedSchema
48
- this.mergedSchemaId = schemaId
49
- this.jsonPointer = jsonPointer
20
+ return this.schemaId + this.jsonPointer
50
21
  }
51
22
  }
52
23
 
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ const { mergeSchemas: _mergeSchemas } = require('@fastify/merge-json-schemas')
4
+
5
+ function mergeSchemas (schemas) {
6
+ return _mergeSchemas(schemas, { onConflict: 'skip' })
7
+ }
8
+
9
+ module.exports = mergeSchemas
package/lib/serializer.js CHANGED
@@ -99,29 +99,20 @@ module.exports = class Serializer {
99
99
  }
100
100
 
101
101
  asString (str) {
102
- if (typeof str !== 'string') {
103
- if (str === null) {
104
- return '""'
105
- }
106
- if (str instanceof Date) {
107
- return '"' + str.toISOString() + '"'
108
- }
109
- if (str instanceof RegExp) {
110
- str = str.source
111
- } else {
112
- str = str.toString()
113
- }
114
- }
115
-
116
102
  if (str.length < 42) {
117
103
  return this.asStringSmall(str)
118
- } else if (STR_ESCAPE.test(str) === false) {
104
+ } else if (str.length < 5000 && STR_ESCAPE.test(str) === false) {
105
+ // Only use the regular expression for shorter input. The overhead is otherwise too much.
119
106
  return '"' + str + '"'
120
107
  } else {
121
108
  return JSON.stringify(str)
122
109
  }
123
110
  }
124
111
 
112
+ asUnsafeString (str) {
113
+ return '"' + str + '"'
114
+ }
115
+
125
116
  // magically escape strings for json
126
117
  // relying on their charCodeAt
127
118
  // everything below 32 needs JSON.stringify()
@@ -137,11 +128,8 @@ module.exports = class Serializer {
137
128
  // eslint-disable-next-line
138
129
  for (var i = 0; i < len; i++) {
139
130
  point = str.charCodeAt(i)
140
- if (point < 32) {
141
- return JSON.stringify(str)
142
- }
143
- if (point >= 0xD800 && point <= 0xDFFF) {
144
- // The current character is a surrogate.
131
+ if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
132
+ // The current character is non-printable characters or a surrogate.
145
133
  return JSON.stringify(str)
146
134
  }
147
135
  if (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.11.1",
3
+ "version": "5.13.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -51,13 +51,13 @@
51
51
  "fast-json-stringify": "."
52
52
  },
53
53
  "dependencies": {
54
- "@fastify/deepmerge": "^1.0.0",
55
54
  "ajv": "^8.10.0",
56
55
  "ajv-formats": "^2.1.1",
57
56
  "fast-deep-equal": "^3.1.3",
58
57
  "fast-uri": "^2.1.0",
59
58
  "rfdc": "^1.2.0",
60
- "json-schema-ref-resolver": "^1.0.1"
59
+ "json-schema-ref-resolver": "^1.0.1",
60
+ "@fastify/merge-json-schemas": "^0.1.0"
61
61
  },
62
62
  "standard": {
63
63
  "ignore": [
@@ -121,7 +121,7 @@ test('object with allOf and multiple schema on the allOf', (t) => {
121
121
  id: 1,
122
122
  name: 'string',
123
123
  tag: 'otherString'
124
- }), '{"name":"string","tag":"otherString","id":1}')
124
+ }), '{"name":"string","id":1,"tag":"otherString"}')
125
125
  })
126
126
 
127
127
  test('object with allOf and one schema on the allOf', (t) => {
@@ -553,8 +553,60 @@ test('allof with local anchor reference', (t) => {
553
553
  t.equal(stringify(data), JSON.stringify(data))
554
554
  })
555
555
 
556
+ test('allOf: multiple nested $ref properties', (t) => {
557
+ t.plan(2)
558
+
559
+ const externalSchema1 = {
560
+ $id: 'externalSchema1',
561
+ oneOf: [
562
+ { $ref: '#/definitions/id1' }
563
+ ],
564
+ definitions: {
565
+ id1: {
566
+ type: 'object',
567
+ properties: {
568
+ id1: {
569
+ type: 'integer'
570
+ }
571
+ },
572
+ additionalProperties: false
573
+ }
574
+ }
575
+ }
576
+
577
+ const externalSchema2 = {
578
+ $id: 'externalSchema2',
579
+ oneOf: [
580
+ { $ref: '#/definitions/id2' }
581
+ ],
582
+ definitions: {
583
+ id2: {
584
+ type: 'object',
585
+ properties: {
586
+ id2: {
587
+ type: 'integer'
588
+ }
589
+ },
590
+ additionalProperties: false
591
+ }
592
+ }
593
+ }
594
+
595
+ const schema = {
596
+ anyOf: [
597
+ { $ref: 'externalSchema1' },
598
+ { $ref: 'externalSchema2' }
599
+ ]
600
+ }
601
+
602
+ const stringify = build(schema, { schema: [externalSchema1, externalSchema2] })
603
+
604
+ t.equal(stringify({ id1: 1 }), JSON.stringify({ id1: 1 }))
605
+ t.equal(stringify({ id2: 2 }), JSON.stringify({ id2: 2 }))
606
+ })
607
+
556
608
  test('allOf: throw Error if types mismatch ', (t) => {
557
- t.plan(1)
609
+ t.plan(3)
558
610
 
559
611
  const schema = {
560
612
  allOf: [
@@ -562,11 +614,18 @@ test('allOf: throw Error if types mismatch ', (t) => {
562
614
  { type: 'number' }
563
615
  ]
564
616
  }
565
- t.throws(() => build(schema), new Error('allOf schemas have different type values'))
617
+ try {
618
+ build(schema)
619
+ t.fail('should throw the MergeError')
620
+ } catch (error) {
621
+ t.ok(error instanceof Error)
622
+ t.equal(error.message, 'Failed to merge "type" keyword schemas.')
623
+ t.same(error.schemas, [['string'], ['number']])
624
+ }
566
625
  })
567
626
 
568
627
  test('allOf: throw Error if format mismatch ', (t) => {
569
- t.plan(1)
628
+ t.plan(3)
570
629
 
571
630
  const schema = {
572
631
  allOf: [
@@ -574,29 +633,87 @@ test('allOf: throw Error if format mismatch ', (t) => {
574
633
  { format: 'time' }
575
634
  ]
576
635
  }
577
- t.throws(() => build(schema), new Error('allOf schemas have different format values'))
636
+ try {
637
+ build(schema)
638
+ t.fail('should throw the MergeError')
639
+ } catch (error) {
640
+ t.ok(error instanceof Error)
641
+ t.equal(error.message, 'Failed to merge "format" keyword schemas.')
642
+ t.same(error.schemas, ['date', 'time'])
643
+ }
578
644
  })
579
645
 
580
- test('allOf: throw Error if nullable mismatch /1', (t) => {
646
+ test('recursive nested allOfs', (t) => {
581
647
  t.plan(1)
582
648
 
583
649
  const schema = {
584
- allOf: [
585
- { nullable: true },
586
- { nullable: false }
587
- ]
650
+ type: 'object',
651
+ properties: {
652
+ foo: {
653
+ additionalProperties: false,
654
+ allOf: [{ $ref: '#' }]
655
+ }
656
+ }
588
657
  }
589
- t.throws(() => build(schema), new Error('allOf schemas have different nullable values'))
658
+
659
+ const data = { foo: {} }
660
+ const stringify = build(schema)
661
+ t.equal(stringify(data), JSON.stringify(data))
590
662
  })
591
663
 
592
- test('allOf: throw Error if nullable mismatch /2', (t) => {
664
+ test('recursive nested allOfs', (t) => {
593
665
  t.plan(1)
594
666
 
595
667
  const schema = {
596
- allOf: [
597
- { nullable: false },
598
- { nullable: true }
599
- ]
668
+ type: 'object',
669
+ properties: {
670
+ foo: {
671
+ additionalProperties: false,
672
+ allOf: [{ allOf: [{ $ref: '#' }] }]
673
+ }
674
+ }
675
+ }
676
+
677
+ const data = { foo: {} }
678
+ const stringify = build(schema)
679
+ t.equal(stringify(data), JSON.stringify(data))
680
+ })
681
+
682
+ test('external recursive allOfs', (t) => {
683
+ t.plan(1)
684
+
685
+ const externalSchema = {
686
+ type: 'object',
687
+ properties: {
688
+ foo: {
689
+ properties: {
690
+ bar: { type: 'string' }
691
+ },
692
+ allOf: [{ $ref: '#' }]
693
+ }
694
+ }
695
+ }
696
+
697
+ const schema = {
698
+ type: 'object',
699
+ properties: {
700
+ a: { $ref: 'externalSchema#/properties/foo' },
701
+ b: { $ref: 'externalSchema#/properties/foo' }
702
+ }
703
+ }
704
+
705
+ const data = {
706
+ a: {
707
+ foo: {},
708
+ bar: '42',
709
+ baz: 42
710
+ },
711
+ b: {
712
+ foo: {},
713
+ bar: '42',
714
+ baz: 42
715
+ }
600
716
  }
601
- t.throws(() => build(schema), new Error('allOf schemas have different nullable values'))
717
+ const stringify = build(schema, { schema: { externalSchema } })
718
+ t.equal(stringify(data), '{"a":{"bar":"42","foo":{}},"b":{"bar":"42","foo":{}}}')
602
719
  })
@@ -644,3 +644,151 @@ test('object with ref and validated properties', (t) => {
644
644
  const stringify = build(schema, { schema: externalSchemas })
645
645
  t.equal(stringify({ id: 1, reference: 'hi' }), '{"id":1,"reference":"hi"}')
646
646
  })
647
+
648
+ test('anyOf required props', (t) => {
649
+ t.plan(3)
650
+
651
+ const schema = {
652
+ type: 'object',
653
+ properties: {
654
+ prop1: { type: 'string' },
655
+ prop2: { type: 'string' },
656
+ prop3: { type: 'string' }
657
+ },
658
+ required: ['prop1'],
659
+ anyOf: [{ required: ['prop2'] }, { required: ['prop3'] }]
660
+ }
661
+ const stringify = build(schema)
662
+ t.equal(stringify({ prop1: 'test', prop2: 'test2' }), '{"prop1":"test","prop2":"test2"}')
663
+ t.equal(stringify({ prop1: 'test', prop3: 'test3' }), '{"prop1":"test","prop3":"test3"}')
664
+ t.equal(stringify({ prop1: 'test', prop2: 'test2', prop3: 'test3' }), '{"prop1":"test","prop2":"test2","prop3":"test3"}')
665
+ })
666
+
667
+ test('anyOf required props', (t) => {
668
+ t.plan(3)
669
+
670
+ const schema = {
671
+ type: 'object',
672
+ properties: {
673
+ prop1: { type: 'string' }
674
+ },
675
+ anyOf: [
676
+ {
677
+ properties: {
678
+ prop2: { type: 'string' }
679
+ }
680
+ },
681
+ {
682
+ properties: {
683
+ prop3: { type: 'string' }
684
+ }
685
+ }
686
+ ]
687
+ }
688
+ const stringify = build(schema)
689
+ t.equal(stringify({ prop1: 'test1' }), '{"prop1":"test1"}')
690
+ t.equal(stringify({ prop2: 'test2' }), '{"prop2":"test2"}')
691
+ t.equal(stringify({ prop1: 'test1', prop2: 'test2' }), '{"prop1":"test1","prop2":"test2"}')
692
+ })
693
+
694
+ test('recursive nested anyOfs', (t) => {
695
+ t.plan(1)
696
+
697
+ const schema = {
698
+ type: 'object',
699
+ properties: {
700
+ foo: {
701
+ additionalProperties: false,
702
+ anyOf: [{ $ref: '#' }]
703
+ }
704
+ }
705
+ }
706
+
707
+ const data = { foo: {} }
708
+ const stringify = build(schema)
709
+ t.equal(stringify(data), JSON.stringify(data))
710
+ })
711
+
712
+ test('recursive nested anyOfs', (t) => {
713
+ t.plan(1)
714
+
715
+ const schema = {
716
+ type: 'object',
717
+ properties: {
718
+ foo: {
719
+ additionalProperties: false,
720
+ anyOf: [{ anyOf: [{ $ref: '#' }] }]
721
+ }
722
+ }
723
+ }
724
+
725
+ const data = { foo: {} }
726
+ const stringify = build(schema)
727
+ t.equal(stringify(data), JSON.stringify(data))
728
+ })
729
+
730
+ test('external recursive anyOfs', (t) => {
731
+ t.plan(1)
732
+
733
+ const externalSchema = {
734
+ type: 'object',
735
+ properties: {
736
+ foo: {
737
+ properties: {
738
+ bar: { type: 'string' }
739
+ },
740
+ anyOf: [{ $ref: '#' }]
741
+ }
742
+ }
743
+ }
744
+
745
+ const schema = {
746
+ type: 'object',
747
+ properties: {
748
+ a: { $ref: 'externalSchema#/properties/foo' },
749
+ b: { $ref: 'externalSchema#/properties/foo' }
750
+ }
751
+ }
752
+
753
+ const data = {
754
+ a: {
755
+ foo: {},
756
+ bar: '42',
757
+ baz: 42
758
+ },
759
+ b: {
760
+ foo: {},
761
+ bar: '42',
762
+ baz: 42
763
+ }
764
+ }
765
+ const stringify = build(schema, { schema: { externalSchema } })
766
+ t.equal(stringify(data), '{"a":{"bar":"42","foo":{}},"b":{"bar":"42","foo":{}}}')
767
+ })
768
+
769
+ test('should build merged schemas twice', (t) => {
770
+ t.plan(2)
771
+
772
+ const schema = {
773
+ type: 'object',
774
+ properties: {
775
+ enums: {
776
+ type: 'string',
777
+ anyOf: [
778
+ { type: 'string', const: 'FOO' },
779
+ { type: 'string', const: 'BAR' }
780
+ ]
781
+ }
782
+ }
783
+ }
784
+
785
+ {
786
+ const stringify = build(schema)
787
+ t.equal(stringify({ enums: 'FOO' }), '{"enums":"FOO"}')
788
+ }
789
+
790
+ {
791
+ const stringify = build(schema)
792
+ t.equal(stringify({ enums: 'BAR' }), '{"enums":"BAR"}')
793
+ }
794
+ })
@@ -18,6 +18,12 @@ function buildTest (schema, toStringify) {
18
18
  })
19
19
  }
20
20
 
21
+ buildTest({
22
+ title: 'string',
23
+ type: 'string',
24
+ format: 'unsafe'
25
+ }, 'hello world')
26
+
21
27
  buildTest({
22
28
  title: 'basic',
23
29
  type: 'object',
@@ -249,15 +249,7 @@ const alphabetOutput = JSON.stringify({
249
249
  const deepFoobarOutput = JSON.stringify({
250
250
  foobar: JSON.parse(foobarOutput)
251
251
  })
252
- const noElseGreetingOutput = JSON.stringify({
253
- kind: 'greeting',
254
- foo: 'FOO',
255
- bar: 42,
256
- hi: 'HI',
257
- hello: 45,
258
- a: 'A',
259
- b: 35
260
- })
252
+ const noElseGreetingOutput = JSON.stringify({})
261
253
 
262
254
  t.test('if-then-else', t => {
263
255
  const tests = [
@@ -419,3 +411,60 @@ t.test('if/else with array', (t) => {
419
411
  t.equal(stringify(['1']), JSON.stringify(['1']))
420
412
  t.equal(stringify(['1', '2']), JSON.stringify([1, 2]))
421
413
  })
414
+
415
+ t.test('external recursive if/then/else', (t) => {
416
+ t.plan(1)
417
+
418
+ const externalSchema = {
419
+ type: 'object',
420
+ properties: {
421
+ base: { type: 'string' },
422
+ self: { $ref: 'externalSchema#' }
423
+ },
424
+ if: {
425
+ type: 'object',
426
+ properties: {
427
+ foo: { type: 'string', const: '41' }
428
+ }
429
+ },
430
+ then: {
431
+ type: 'object',
432
+ properties: {
433
+ bar: { type: 'string', const: '42' }
434
+ }
435
+ },
436
+ else: {
437
+ type: 'object',
438
+ properties: {
439
+ baz: { type: 'string', const: '43' }
440
+ }
441
+ }
442
+ }
443
+
444
+ const schema = {
445
+ type: 'object',
446
+ properties: {
447
+ a: { $ref: 'externalSchema#/properties/self' },
448
+ b: { $ref: 'externalSchema#/properties/self' }
449
+ }
450
+ }
451
+
452
+ const data = {
453
+ a: {
454
+ base: 'a',
455
+ foo: '41',
456
+ bar: '42',
457
+ baz: '43',
458
+ ignore: 'ignored'
459
+ },
460
+ b: {
461
+ base: 'b',
462
+ foo: 'not-41',
463
+ bar: '42',
464
+ baz: '43',
465
+ ignore: 'ignored'
466
+ }
467
+ }
468
+ const stringify = build(schema, { schema: { externalSchema } })
469
+ t.equal(stringify(data), '{"a":{"base":"a","bar":"42"},"b":{"base":"b","baz":"43"}}')
470
+ })
@@ -126,17 +126,18 @@ test('object with multiple required field not in properties schema', (t) => {
126
126
  stringify({})
127
127
  t.fail()
128
128
  } catch (e) {
129
- t.equal(e.message, '"num" is required!')
129
+ t.equal(e.message, '"key1" is required!')
130
130
  t.pass()
131
131
  }
132
132
 
133
133
  try {
134
134
  stringify({
135
- num: 42
135
+ key1: 42,
136
+ key2: 42
136
137
  })
137
138
  t.fail()
138
139
  } catch (e) {
139
- t.equal(e.message, '"key1" is required!')
140
+ t.equal(e.message, '"num" is required!')
140
141
  t.pass()
141
142
  }
142
143
 
@@ -48,3 +48,37 @@ test('serialize long string', (t) => {
48
48
  t.equal(output, `"${new Array(2e4).fill('\\u0000').join('')}"`)
49
49
  t.equal(JSON.parse(output), input)
50
50
  })
51
+
52
+ test('unsafe string', (t) => {
53
+ t.plan(2)
54
+
55
+ const schema = {
56
+ type: 'string',
57
+ format: 'unsafe'
58
+ }
59
+
60
+ const input = 'abcd'
61
+ const stringify = build(schema)
62
+ const output = stringify(input)
63
+
64
+ t.equal(output, `"${input}"`)
65
+ t.equal(JSON.parse(output), input)
66
+ })
67
+
68
+ test('unsafe unescaped string', (t) => {
69
+ t.plan(2)
70
+
71
+ const schema = {
72
+ type: 'string',
73
+ format: 'unsafe'
74
+ }
75
+
76
+ const input = 'abcd "abcd"'
77
+ const stringify = build(schema)
78
+ const output = stringify(input)
79
+
80
+ t.equal(output, `"${input}"`)
81
+ t.throws(function () {
82
+ JSON.parse(output)
83
+ })
84
+ })
package/types/index.d.ts CHANGED
@@ -208,6 +208,9 @@ interface StandaloneOption extends build.Options {
208
208
  mode: 'standalone'
209
209
  }
210
210
 
211
+ type StringCoercible = string | Date | RegExp;
212
+ type IntegerCoercible = number | BigInt;
213
+
211
214
  /**
212
215
  * Build a stringify function using a schema of the documents that should be stringified
213
216
  * @param schema The schema used to stringify values
@@ -217,8 +220,8 @@ declare function build(schema: build.AnySchema, options: DebugOption): { code: s
217
220
  declare function build(schema: build.AnySchema, options: DeprecateDebugOption): { code: string, ajv: Ajv };
218
221
  declare function build(schema: build.AnySchema, options: StandaloneOption): string;
219
222
  declare function build(schema: build.AnySchema, options?: build.Options): <TDoc = any>(doc: TDoc) => any;
220
- declare function build(schema: build.StringSchema, options?: build.Options): <TDoc extends string = string>(doc: TDoc) => string;
221
- declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): <TDoc extends number = number>(doc: TDoc) => string;
223
+ declare function build(schema: build.StringSchema, options?: build.Options): <TDoc extends StringCoercible = StringCoercible>(doc: TDoc) => string;
224
+ declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): <TDoc extends IntegerCoercible = IntegerCoercible>(doc: TDoc) => string;
222
225
  declare function build(schema: build.NullSchema, options?: build.Options): <TDoc extends null = null>(doc: TDoc) => "null";
223
226
  declare function build(schema: build.BooleanSchema, options?: build.Options): <TDoc extends boolean = boolean>(doc: TDoc) => string;
224
227
  declare function build(schema: build.ArraySchema | build.TupleSchema, options?: build.Options): <TDoc extends any[]= any[]>(doc: TDoc) => string;