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
@@ -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
  test('schema with const string', (t) => {
@@ -24,6 +24,26 @@ test('schema with const string', (t) => {
24
24
  t.assert.ok(validate(JSON.parse(output)), 'valid schema')
25
25
  })
26
26
 
27
+ test('schema with const string containing characters that need escaping', (t) => {
28
+ const values = ['back\\slash', 'tab\there', 'quote"here', 'new\nline', "tick's"]
29
+ t.plan(values.length * 2)
30
+
31
+ for (const value of values) {
32
+ const schema = {
33
+ type: 'object',
34
+ properties: {
35
+ foo: { const: value }
36
+ }
37
+ }
38
+
39
+ const stringify = build(schema)
40
+ const output = stringify({ foo: value })
41
+
42
+ t.assert.equal(output, JSON.stringify({ foo: value }))
43
+ t.assert.deepStrictEqual(JSON.parse(output), { foo: value })
44
+ }
45
+ })
46
+
27
47
  test('schema with const string and different input', (t) => {
28
48
  t.plan(2)
29
49
 
@@ -259,7 +279,9 @@ test('schema with const and null as type', (t) => {
259
279
  })
260
280
 
261
281
  t.assert.equal(output, '{"foo":null}')
262
- t.assert.ok(validate(JSON.parse(output)), 'valid schema')
282
+ // `const` applies regardless of `type` in JSON Schema, so ajv rejects null
283
+ // here even though the serializer lets it through because the type allows it.
284
+ t.assert.equal(validate(JSON.parse(output)), false)
263
285
 
264
286
  const output2 = stringify({ foo: 'baz' })
265
287
  t.assert.equal(output2, '{"foo":"baz"}')
@@ -267,7 +289,7 @@ test('schema with const and null as type', (t) => {
267
289
  })
268
290
 
269
291
  test('schema with const as nullable', (t) => {
270
- t.plan(4)
292
+ t.plan(2)
271
293
 
272
294
  const schema = {
273
295
  type: 'object',
@@ -276,20 +298,19 @@ test('schema with const as nullable', (t) => {
276
298
  }
277
299
  }
278
300
 
279
- const validate = validator(schema)
301
+ // ajv refuses to compile `nullable` without a `type`, so the output is not
302
+ // validated against the schema in this test.
280
303
  const stringify = build(schema)
281
304
  const output = stringify({
282
305
  foo: null
283
306
  })
284
307
 
285
308
  t.assert.equal(output, '{"foo":null}')
286
- t.assert.ok(validate(JSON.parse(output)), 'valid schema')
287
309
 
288
310
  const output2 = stringify({
289
311
  foo: 'baz'
290
312
  })
291
313
  t.assert.equal(output2, '{"foo":"baz"}')
292
- t.assert.ok(validate(JSON.parse(output2)), 'valid schema')
293
314
  })
294
315
 
295
316
  test('schema with const and invalid object', (t) => {
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