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/test/date.test.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { test } = require('node:test')
4
- const validator = require('is-my-json-valid')
4
+ const validator = require('./utils/validator')
5
5
  const build = require('..')
6
6
 
7
7
  process.env.TZ = 'UTC'
@@ -637,3 +637,30 @@ test('should serialize also an invalid string value, even if it is not a valid t
637
637
  t.assert.equal(output, JSON.stringify(toStringify))
638
638
  t.assert.equal(validate(JSON.parse(output)), false, 'valid schema')
639
639
  })
640
+
641
+ test('should escape strings that are not valid dates', (t) => {
642
+ const formats = ['date-time', 'date', 'time']
643
+ const values = [
644
+ '2026-01-01T00:00:00Z","admin":true,"x":"',
645
+ 'back\\slash',
646
+ 'new\nline',
647
+ 'lone \ud800 surrogate'
648
+ ]
649
+
650
+ t.plan(formats.length * values.length * 2)
651
+
652
+ for (const format of formats) {
653
+ const stringify = build({
654
+ type: 'object',
655
+ properties: {
656
+ ts: { type: 'string', format }
657
+ }
658
+ })
659
+
660
+ for (const value of values) {
661
+ const output = stringify({ ts: value })
662
+ t.assert.equal(output, `{"ts":${JSON.stringify(value)}}`)
663
+ t.assert.deepStrictEqual(JSON.parse(output), { ts: value })
664
+ }
665
+ }
666
+ })
@@ -128,3 +128,11 @@ test('Validator restoreFromState', t => {
128
128
  const restored = Validator.restoreFromState(state)
129
129
  t.assert.ok(restored instanceof Validator)
130
130
  })
131
+
132
+ test('Validator ignores null schemas while converting nested values', t => {
133
+ t.plan(1)
134
+
135
+ const validator = new Validator()
136
+
137
+ t.assert.doesNotThrow(() => validator.convertSchemaToAjvFormat(null))
138
+ })
@@ -0,0 +1,251 @@
1
+ 'use strict'
2
+
3
+ // V8 has to optimize the generated serializers and keep them optimized when they see again the
4
+ // values they were optimized for. To see why a case fails: node --trace-deopt test/deopt.test.js
5
+
6
+ const { test } = require('node:test')
7
+ const v8 = require('node:v8')
8
+
9
+ // before the library loads, so its functions collect feedback from the first call
10
+ v8.setFlagsFromString('--allow-natives-syntax')
11
+ v8.setFlagsFromString('--no-lazy-feedback-allocation')
12
+
13
+ const build = require('..')
14
+
15
+ /* eslint no-new-func: "off" */
16
+ const prepare = new Function('fn', '%PrepareFunctionForOptimization(fn)')
17
+ const optimize = new Function('fn', '%OptimizeFunctionOnNextCall(fn)')
18
+ const isOptimized = new Function('fn', 'return %ActiveTierIsTurbofan(fn)')
19
+
20
+ function assertStaysOptimized (t, stringify, inputs) {
21
+ prepare(stringify)
22
+ for (const input of inputs) stringify(input)
23
+ optimize(stringify)
24
+ stringify(inputs[0])
25
+ t.assert.ok(isOptimized(stringify), 'V8 did not optimize it')
26
+
27
+ for (let round = 0; round < 100; round++) {
28
+ for (let i = 0; i < inputs.length; i++) {
29
+ stringify(inputs[i])
30
+ if (!isOptimized(stringify)) t.assert.fail(`deoptimized by input ${i} in round ${round}`)
31
+ }
32
+ }
33
+ }
34
+
35
+ test('string', (t) => {
36
+ assertStaysOptimized(t, build({ type: 'string' }), [
37
+ 'hello', 'say "hi"', 'π', '😀', 'line\nbreak', '',
38
+ 'x'.repeat(100), 'y"'.repeat(50), 'z'.repeat(6000), null, new Date(0), /re/, 42
39
+ ])
40
+ })
41
+
42
+ test('string formats', (t) => {
43
+ const stringify = build({
44
+ type: 'object',
45
+ properties: {
46
+ dateTime: { type: 'string', format: 'date-time' },
47
+ date: { type: 'string', format: 'date' },
48
+ time: { type: 'string', format: 'time' },
49
+ unsafe: { type: 'string', format: 'unsafe' }
50
+ }
51
+ })
52
+ assertStaysOptimized(t, stringify, [
53
+ { dateTime: new Date(0), date: new Date(0), time: new Date(0), unsafe: 'raw' },
54
+ { dateTime: '2020-01-01T00:00:00.000Z', date: '2020-01-01', time: '10:00:00', unsafe: 'raw' },
55
+ { dateTime: null, date: null, time: null }
56
+ ])
57
+ })
58
+
59
+ test('number', (t) => {
60
+ assertStaysOptimized(t, build({ type: 'number' }), [42, -1.5, 1e21, Infinity, '42'])
61
+ })
62
+
63
+ test('integer', (t) => {
64
+ assertStaysOptimized(t, build({ type: 'integer' }), [42, -42, 4.7, 2 ** 40, 10n])
65
+ })
66
+
67
+ test('integer with rounding', (t) => {
68
+ assertStaysOptimized(t, build({ type: 'integer' }, { rounding: 'round' }), [42, 4.7, -4.2])
69
+ })
70
+
71
+ test('boolean', (t) => {
72
+ assertStaysOptimized(t, build({ type: 'boolean' }), [true, false, 0, 'yes'])
73
+ })
74
+
75
+ test('object', (t) => {
76
+ const stringify = build({
77
+ type: 'object',
78
+ properties: {
79
+ id: { type: 'integer' },
80
+ name: { type: 'string' },
81
+ score: { type: 'number' },
82
+ active: { type: 'boolean' },
83
+ role: { type: 'string', default: 'user' },
84
+ note: { type: 'string', nullable: true },
85
+ address: {
86
+ type: 'object',
87
+ properties: { city: { type: 'string' }, zip: { type: 'integer' } }
88
+ }
89
+ },
90
+ required: ['id', 'name']
91
+ })
92
+ assertStaysOptimized(t, stringify, [
93
+ { id: 1, name: 'a', score: 1.5, active: true, role: 'admin', note: 'n', address: { city: 'c', zip: 1 } },
94
+ { id: 2, name: 'b', score: 2, note: null, address: null },
95
+ { id: 3, name: 'c' }
96
+ ])
97
+ })
98
+
99
+ test('array', (t) => {
100
+ const stringify = build({
101
+ type: 'object',
102
+ properties: {
103
+ numbers: { type: 'array', items: { type: 'number' } },
104
+ strings: { type: 'array', items: { type: 'string' } },
105
+ objects: {
106
+ type: 'array',
107
+ items: { type: 'object', properties: { s: { type: 'string' }, n: { type: 'number' } } }
108
+ },
109
+ tuple: { type: 'array', items: [{ type: 'string' }, { type: 'integer' }] },
110
+ open: { type: 'array', items: [{ type: 'string' }], additionalItems: true }
111
+ }
112
+ })
113
+ assertStaysOptimized(t, stringify, [
114
+ {
115
+ numbers: [1, 2.5],
116
+ strings: ['a', 'b"'],
117
+ objects: [{ s: 'a', n: 1 }, { s: 'b', n: 2.5 }],
118
+ tuple: ['a', 1],
119
+ open: ['a', 1, true]
120
+ },
121
+ { numbers: [], strings: [], objects: [], tuple: ['a'], open: [] }
122
+ ])
123
+ })
124
+
125
+ test('large array mechanism', (t) => {
126
+ const stringify = build(
127
+ { type: 'array', items: { type: 'integer' } },
128
+ { largeArrayMechanism: 'json-stringify', largeArraySize: 3 }
129
+ )
130
+ assertStaysOptimized(t, stringify, [[1, 2], [1, 2, 3, 4]])
131
+ })
132
+
133
+ test('additionalProperties and patternProperties', (t) => {
134
+ const stringify = build({
135
+ type: 'object',
136
+ properties: { a: { type: 'string' } },
137
+ patternProperties: { '^n': { type: 'number' } },
138
+ additionalProperties: { type: 'string' }
139
+ })
140
+ assertStaysOptimized(t, stringify, [{ a: 'x', n1: 1, n2: 2.5, other: 'y' }, { other: 'y' }])
141
+ })
142
+
143
+ test('additionalProperties true', (t) => {
144
+ const stringify = build({ type: 'object', additionalProperties: true })
145
+ assertStaysOptimized(t, stringify, [{ a: 'x', b: 1, c: [1, { d: null }] }, {}])
146
+ })
147
+
148
+ test('multiple types', (t) => {
149
+ const stringify = build({
150
+ type: 'object',
151
+ properties: {
152
+ value: { type: ['string', 'integer', 'null'] },
153
+ list: { type: ['array', 'object'], items: { type: 'integer' }, properties: { a: { type: 'integer' } } }
154
+ }
155
+ })
156
+ assertStaysOptimized(t, stringify, [
157
+ { value: 'a', list: [1, 2] },
158
+ { value: 1, list: { a: 1 } },
159
+ { value: null }
160
+ ])
161
+ })
162
+
163
+ test('anyOf and oneOf', (t) => {
164
+ const stringify = build({
165
+ type: 'object',
166
+ properties: {
167
+ any: { anyOf: [{ type: 'string' }, { type: 'number' }] },
168
+ one: {
169
+ oneOf: [
170
+ { type: 'object', properties: { kind: { const: 'a' }, a: { type: 'string' } }, required: ['kind'] },
171
+ { type: 'object', properties: { kind: { const: 'b' }, b: { type: 'number' } }, required: ['kind'] }
172
+ ]
173
+ }
174
+ }
175
+ })
176
+ assertStaysOptimized(t, stringify, [
177
+ { any: 'x', one: { kind: 'a', a: 'x' } },
178
+ { any: 1, one: { kind: 'b', b: 1 } }
179
+ ])
180
+ })
181
+
182
+ test('if then else', (t) => {
183
+ const stringify = build({
184
+ type: 'object',
185
+ properties: { kind: { type: 'string' } },
186
+ if: { type: 'object', properties: { kind: { const: 'a' } } },
187
+ then: { properties: { a: { type: 'string' } } },
188
+ else: { properties: { b: { type: 'number' } } }
189
+ })
190
+ assertStaysOptimized(t, stringify, [{ kind: 'a', a: 'x' }, { kind: 'b', b: 1 }])
191
+ })
192
+
193
+ test('allOf', (t) => {
194
+ const stringify = build({
195
+ allOf: [
196
+ { type: 'object', properties: { a: { type: 'string' } } },
197
+ { properties: { b: { type: 'number' } } }
198
+ ]
199
+ })
200
+ assertStaysOptimized(t, stringify, [{ a: 'x', b: 1 }, {}])
201
+ })
202
+
203
+ test('recursive ref', (t) => {
204
+ const stringify = build({
205
+ $id: 'tree',
206
+ type: 'object',
207
+ properties: {
208
+ value: { type: 'integer' },
209
+ children: { type: 'array', items: { $ref: 'tree#' } }
210
+ }
211
+ })
212
+ assertStaysOptimized(t, stringify, [
213
+ { value: 1, children: [{ value: 2, children: [] }, { value: 3, children: [{ value: 4 }] }] },
214
+ { value: 5 }
215
+ ])
216
+ })
217
+
218
+ test('external ref', (t) => {
219
+ const defs = {
220
+ $id: 'defs',
221
+ definitions: { item: { type: 'object', properties: { x: { type: 'string' } } } }
222
+ }
223
+ const stringify = build(
224
+ { type: 'object', properties: { item: { $ref: 'defs#/definitions/item' } } },
225
+ { schema: { defs } }
226
+ )
227
+ assertStaysOptimized(t, stringify, [{ item: { x: 'y' } }, {}])
228
+ })
229
+
230
+ test('toJSON', (t) => {
231
+ const stringify = build({ type: 'object', properties: { a: { type: 'string' } } })
232
+ assertStaysOptimized(t, stringify, [{ a: 'x' }, { toJSON () { return { a: 'y' } } }])
233
+ })
234
+
235
+ // V8 does not optimize a function over a size, so a serializer whose code grows too much with
236
+ // every property stops being optimized for a wide object like this one
237
+ test('wide object', (t) => {
238
+ const properties = {}
239
+ const input = {}
240
+ for (let i = 0; i < 300; i++) {
241
+ switch (i % 4) {
242
+ case 0: properties[`s${i}`] = { type: 'string' }; input[`s${i}`] = 'x'; break
243
+ case 1: properties[`n${i}`] = { type: 'number' }; input[`n${i}`] = i; break
244
+ case 2: properties[`b${i}`] = { type: 'boolean' }; input[`b${i}`] = true; break
245
+ case 3:
246
+ properties[`o${i}`] = { type: 'object', properties: { a: { type: 'string' }, b: { type: 'integer' } } }
247
+ input[`o${i}`] = { a: 'y', b: i }
248
+ }
249
+ }
250
+ assertStaysOptimized(t, build({ type: 'object', properties }), [input, {}])
251
+ })
@@ -538,3 +538,148 @@ test('if-then-else with allOf', (t) => {
538
538
  t.assert.doesNotThrow(() => JSON.parse(output))
539
539
  t.assert.equal(output, '{"base":"test","foo":"value"}')
540
540
  })
541
+
542
+ test('if with a bare top-level $ref validates directly against the registered schema', (t) => {
543
+ t.plan(2)
544
+
545
+ const schema = {
546
+ type: 'object',
547
+ if: { $ref: 'IsFoo#' },
548
+ then: {
549
+ type: 'object',
550
+ properties: {
551
+ foo: { type: 'string' }
552
+ }
553
+ },
554
+ else: {
555
+ type: 'object',
556
+ properties: {
557
+ bar: { type: 'string' }
558
+ }
559
+ }
560
+ }
561
+ const externalSchema = {
562
+ IsFoo: {
563
+ type: 'object',
564
+ properties: {
565
+ kind: { type: 'string', const: 'foo' }
566
+ },
567
+ required: ['kind']
568
+ }
569
+ }
570
+
571
+ const code = build(schema, { mode: 'standalone', schema: externalSchema })
572
+ t.assert.ok(code.includes('validator.validate("IsFoo#"'), 'validates directly against the bare registered id')
573
+
574
+ const stringify = build(schema, { schema: externalSchema })
575
+ t.assert.equal(stringify({ kind: 'foo', foo: 'value', bar: 'ignored' }), '{"foo":"value"}')
576
+ })
577
+
578
+ test('if with a $ref into a JSON-pointer sub-path validates directly against that sub-path', (t) => {
579
+ t.plan(2)
580
+
581
+ const schema = {
582
+ type: 'object',
583
+ if: { $ref: 'Rules#/definitions/IsFoo' },
584
+ then: {
585
+ type: 'object',
586
+ properties: {
587
+ foo: { type: 'string' }
588
+ }
589
+ },
590
+ else: {
591
+ type: 'object',
592
+ properties: {
593
+ bar: { type: 'string' }
594
+ }
595
+ }
596
+ }
597
+ const externalSchema = {
598
+ Rules: {
599
+ definitions: {
600
+ IsFoo: {
601
+ type: 'object',
602
+ properties: {
603
+ kind: { type: 'string', const: 'foo' }
604
+ },
605
+ required: ['kind']
606
+ }
607
+ }
608
+ }
609
+ }
610
+
611
+ const code = build(schema, { mode: 'standalone', schema: externalSchema })
612
+ t.assert.ok(code.includes('validator.validate("Rules#/definitions/IsFoo"'), 'validates directly against the registered sub-path ref')
613
+
614
+ const stringify = build(schema, { schema: externalSchema })
615
+ t.assert.equal(stringify({ kind: 'foo', foo: 'value', bar: 'ignored' }), '{"foo":"value"}')
616
+ })
617
+
618
+ test('if with a $ref that has sibling keywords is left unchanged', (t) => {
619
+ t.plan(2)
620
+
621
+ const schema = {
622
+ type: 'object',
623
+ if: { $ref: 'IsFoo#', required: ['extra'] },
624
+ then: {
625
+ type: 'object',
626
+ properties: {
627
+ foo: { type: 'string' }
628
+ }
629
+ },
630
+ else: {
631
+ type: 'object',
632
+ properties: {
633
+ bar: { type: 'string' }
634
+ }
635
+ }
636
+ }
637
+ const externalSchema = {
638
+ IsFoo: {
639
+ type: 'object',
640
+ properties: {
641
+ kind: { type: 'string', const: 'foo' }
642
+ },
643
+ required: ['kind']
644
+ }
645
+ }
646
+
647
+ const code = build(schema, { mode: 'standalone', schema: externalSchema })
648
+ t.assert.ok(!code.includes('validator.validate("IsFoo#"'), 'ref with sibling keywords is not redirected')
649
+
650
+ const stringify = build(schema, { schema: externalSchema })
651
+ t.assert.equal(stringify({ kind: 'foo', foo: 'ignored', bar: 'fallback' }), '{"bar":"fallback"}')
652
+ })
653
+
654
+ test('if/else without then applies the else branch', (t) => {
655
+ t.plan(4)
656
+
657
+ const schema = {
658
+ type: 'object',
659
+ properties: {
660
+ kind: { type: 'string' }
661
+ },
662
+ if: {
663
+ type: 'object',
664
+ properties: {
665
+ kind: { type: 'string', enum: ['foobar'] }
666
+ }
667
+ },
668
+ else: {
669
+ type: 'object',
670
+ properties: {
671
+ bar: { type: 'string' }
672
+ }
673
+ }
674
+ }
675
+
676
+ const stringify = build(schema)
677
+
678
+ const elseOutput = stringify({ kind: 'other', bar: 'value' })
679
+ t.assert.equal(elseOutput, '{"kind":"other","bar":"value"}')
680
+ t.assert.deepStrictEqual(JSON.parse(elseOutput), { kind: 'other', bar: 'value' })
681
+
682
+ const thenOutput = stringify({ kind: 'foobar', bar: 'value' })
683
+ t.assert.equal(thenOutput, '{"kind":"foobar"}')
684
+ t.assert.deepStrictEqual(JSON.parse(thenOutput), { kind: 'foobar' })
685
+ })
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { test } = require('node:test')
4
- const validator = require('is-my-json-valid')
4
+ const validator = require('./utils/validator')
5
5
  const build = require('..')
6
6
 
7
7
  function buildTest (schema, toStringify) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { test } = require('node:test')
4
4
 
5
- const validator = require('is-my-json-valid')
5
+ const validator = require('./utils/validator')
6
6
  const build = require('..')
7
7
  const ROUNDING_TYPES = ['ceil', 'floor', 'round']
8
8