fast-json-stringify 7.1.0 → 7.1.1

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.
@@ -1,6 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ const assert = require('node:assert/strict')
3
4
  const { Bench } = require('tinybench')
5
+ const apiResponses = require('./fixtures/api-responses')
4
6
  const suite = new Bench({
5
7
  name: 'Library Comparison Benchmarks',
6
8
  setup: (_task, mode) => {
@@ -173,139 +175,162 @@ for (let i = 0; i < MULTI_ARRAY_LENGTH; i++) {
173
175
  multiArray[i] = obj
174
176
  }
175
177
 
176
- suite.add('fast-json-stringify: creation', function () {
177
- FJS(schema)
178
+ // Some serializers return unflattened strings. Fastify calls Buffer.byteLength()
179
+ // for Content-Length, which triggers flattening; include that cost in every case.
180
+ suite.add('fast-json-stringify: creation and first stringify', function () {
181
+ Buffer.byteLength(FJS(schema)(obj))
178
182
  })
179
- suite.add('compile-json-stringify: creation', function () {
180
- CJS(schemaCJS)
183
+ suite.add('compile-json-stringify: creation and first stringify', function () {
184
+ Buffer.byteLength(CJS(schemaCJS)(obj))
181
185
  })
182
- suite.add('AJV: creation', function () {
183
- ajv.compileSerializer(schemaAJVJTD)
186
+ suite.add('AJV: creation and first stringify', function () {
187
+ Buffer.byteLength(ajv.compileSerializer(schemaAJVJTD)(obj))
184
188
  })
185
- suite.add('json-accelerator: creation', function () {
186
- createAccelerator(schema)
189
+ suite.add('json-accelerator: creation and first stringify', function () {
190
+ Buffer.byteLength(createAccelerator(schema)(obj))
187
191
  })
188
192
 
189
193
  suite.add('JSON.stringify: array', function () {
190
- JSON.stringify(multiArray)
194
+ Buffer.byteLength(JSON.stringify(multiArray))
191
195
  })
192
196
 
193
197
  suite.add('fast-json-stringify [default]: array', function () {
194
- stringifyArrayDefault(multiArray)
198
+ Buffer.byteLength(stringifyArrayDefault(multiArray))
195
199
  })
196
200
 
197
201
  suite.add('json-accelerator: array', function () {
198
- accelArray(multiArray)
202
+ Buffer.byteLength(accelArray(multiArray))
199
203
  })
200
204
 
201
205
  suite.add('fast-json-stringify [json-stringify]: array', function () {
202
- stringifyArrayJSONStringify(multiArray)
206
+ Buffer.byteLength(stringifyArrayJSONStringify(multiArray))
203
207
  })
204
208
 
205
209
  suite.add('compile-json-stringify: array', function () {
206
- CJSStringifyArray(multiArray)
210
+ Buffer.byteLength(CJSStringifyArray(multiArray))
207
211
  })
208
212
 
209
213
  suite.add('AJV: array', function () {
210
- ajvSerializeArray(multiArray)
214
+ Buffer.byteLength(ajvSerializeArray(multiArray))
211
215
  })
212
216
 
213
217
  suite.add('JSON.stringify: large array', function () {
214
- JSON.stringify(largeArray)
218
+ Buffer.byteLength(JSON.stringify(largeArray))
215
219
  })
216
220
 
217
221
  suite.add('fast-json-stringify [default]: large array', function () {
218
- stringifyArrayDefault(largeArray)
222
+ Buffer.byteLength(stringifyArrayDefault(largeArray))
219
223
  })
220
224
 
221
225
  suite.add('fast-json-stringify [json-stringify]: large array', function () {
222
- stringifyArrayJSONStringify(largeArray)
226
+ Buffer.byteLength(stringifyArrayJSONStringify(largeArray))
223
227
  })
224
228
 
225
229
  suite.add('compile-json-stringify: large array', function () {
226
- CJSStringifyArray(largeArray)
230
+ Buffer.byteLength(CJSStringifyArray(largeArray))
227
231
  })
228
232
 
229
233
  suite.add('AJV: large array', function () {
230
- ajvSerializeArray(largeArray)
234
+ Buffer.byteLength(ajvSerializeArray(largeArray))
231
235
  })
232
236
 
233
237
  suite.add('JSON.stringify: long string', function () {
234
- JSON.stringify(str)
238
+ Buffer.byteLength(JSON.stringify(str))
235
239
  })
236
240
 
237
241
  suite.add('fast-json-stringify: long string', function () {
238
- stringifyString(str)
242
+ Buffer.byteLength(stringifyString(str))
239
243
  })
240
244
 
241
245
  suite.add('json-accelerator: long string', function () {
242
- stringifyString(str)
246
+ Buffer.byteLength(stringifyString(str))
243
247
  })
244
248
 
245
249
  suite.add('compile-json-stringify: long string', function () {
246
- CJSStringifyString(str)
250
+ Buffer.byteLength(CJSStringifyString(str))
247
251
  })
248
252
 
249
253
  suite.add('AJV: long string', function () {
250
- ajvSerializeString(str)
254
+ Buffer.byteLength(ajvSerializeString(str))
251
255
  })
252
256
 
253
257
  suite.add('JSON.stringify: short string', function () {
254
- JSON.stringify('hello world')
258
+ Buffer.byteLength(JSON.stringify('hello world'))
255
259
  })
256
260
 
257
261
  suite.add('fast-json-stringify: short string', function () {
258
- stringifyString('hello world')
262
+ Buffer.byteLength(stringifyString('hello world'))
259
263
  })
260
264
 
261
265
  suite.add('json-accelerator: short string', function () {
262
- accelString('hello world')
266
+ Buffer.byteLength(accelString('hello world'))
263
267
  })
264
268
 
265
269
  suite.add('compile-json-stringify: short string', function () {
266
- CJSStringifyString('hello world')
270
+ Buffer.byteLength(CJSStringifyString('hello world'))
267
271
  })
268
272
 
269
273
  suite.add('AJV: short string', function () {
270
- ajvSerializeString('hello world')
274
+ Buffer.byteLength(ajvSerializeString('hello world'))
271
275
  })
272
276
 
273
277
  suite.add('JSON.stringify: obj', function () {
274
- JSON.stringify(obj)
278
+ Buffer.byteLength(JSON.stringify(obj))
275
279
  })
276
280
 
277
281
  suite.add('fast-json-stringify: obj', function () {
278
- stringify(obj)
282
+ Buffer.byteLength(stringify(obj))
279
283
  })
280
284
 
281
285
  suite.add('json-accelerator: obj', function () {
282
- accelStringify(obj)
286
+ Buffer.byteLength(accelStringify(obj))
283
287
  })
284
288
 
285
289
  suite.add('compile-json-stringify: obj', function () {
286
- CJSStringify(obj)
290
+ Buffer.byteLength(CJSStringify(obj))
287
291
  })
288
292
 
289
293
  suite.add('AJV: obj', function () {
290
- ajvSerialize(obj)
294
+ Buffer.byteLength(ajvSerialize(obj))
291
295
  })
292
296
 
293
297
  suite.add('JSON.stringify: date', function () {
294
- JSON.stringify(date)
298
+ Buffer.byteLength(JSON.stringify(date))
295
299
  })
296
300
 
297
301
  suite.add('fast-json-stringify: date', function () {
298
- stringifyDate(date)
302
+ Buffer.byteLength(stringifyDate(date))
299
303
  })
300
304
 
301
305
  suite.add('json-accelerate: date', function () {
302
- accelDate(date)
306
+ Buffer.byteLength(accelDate(date))
303
307
  })
304
308
 
305
309
  suite.add('compile-json-stringify: date', function () {
306
- CJSStringifyDate(date)
310
+ Buffer.byteLength(CJSStringifyDate(date))
307
311
  })
308
312
 
313
+ for (const { name, schema: responseSchema, input } of apiResponses) {
314
+ const stringifyResponse = FJS(responseSchema)
315
+ // Check outside the timed loop that the schema preserves the complete response.
316
+ assert.deepStrictEqual(JSON.parse(stringifyResponse(input)), JSON.parse(JSON.stringify(input)))
317
+
318
+ suite.add(`JSON.stringify: ${name}`, function () {
319
+ Buffer.byteLength(JSON.stringify(input))
320
+ })
321
+ suite.add(`fast-json-stringify: ${name}`, function () {
322
+ Buffer.byteLength(stringifyResponse(input))
323
+ })
324
+ }
325
+
326
+ if (process.argv.includes('--fjs-vs-json')) {
327
+ for (const task of suite.tasks) {
328
+ if (!task.name.startsWith('fast-json-stringify') && !task.name.startsWith('JSON.stringify:')) {
329
+ suite.remove(task.name)
330
+ }
331
+ }
332
+ }
333
+
309
334
  suite.run().then(() => {
310
335
  const results = suite.tasks.map(task => ({
311
336
  name: task.name,
@@ -18,10 +18,12 @@ const FJS = require('..')
18
18
  const stringify = benchmark.compile ? null : FJS(benchmark.schema, benchmark.options)
19
19
 
20
20
  bench.add(benchmark.name, () => {
21
+ // Some serializers return unflattened strings. Fastify calls Buffer.byteLength()
22
+ // for Content-Length, which triggers flattening; include that cost here too.
21
23
  if (benchmark.compile) {
22
- FJS(benchmark.schema, benchmark.options)(benchmark.input)
24
+ Buffer.byteLength(FJS(benchmark.schema, benchmark.options)(benchmark.input))
23
25
  } else {
24
- stringify(benchmark.input)
26
+ Buffer.byteLength(stringify(benchmark.input))
25
27
  }
26
28
  }).run().then(() => {
27
29
  const task = bench.tasks[0]
@@ -0,0 +1,266 @@
1
+ 'use strict'
2
+
3
+ const addressSchema = {
4
+ type: 'object',
5
+ properties: {
6
+ street: { type: 'string' },
7
+ city: { type: 'string' },
8
+ postalCode: { type: 'string' },
9
+ country: { type: 'string' },
10
+ apartment: { type: ['string', 'null'] }
11
+ }
12
+ }
13
+
14
+ const productSchema = {
15
+ type: 'object',
16
+ properties: {
17
+ id: { type: 'string' },
18
+ sku: { type: 'string' },
19
+ name: { type: 'string' },
20
+ description: { type: 'string' },
21
+ price: {
22
+ type: 'object',
23
+ properties: {
24
+ amount: { type: 'integer' },
25
+ currency: { type: 'string' },
26
+ discount: { type: ['number', 'null'] }
27
+ }
28
+ },
29
+ inventory: {
30
+ type: 'object',
31
+ properties: {
32
+ available: { type: 'boolean' },
33
+ quantity: { type: 'integer' },
34
+ warehouse: { type: 'string' }
35
+ }
36
+ },
37
+ tags: { type: 'array', items: { type: 'string' } },
38
+ images: {
39
+ type: 'array',
40
+ items: {
41
+ type: 'object',
42
+ properties: {
43
+ url: { type: 'string' },
44
+ alt: { type: 'string' },
45
+ primary: { type: 'boolean' }
46
+ }
47
+ }
48
+ },
49
+ rating: {
50
+ type: 'object',
51
+ properties: {
52
+ average: { type: 'number' },
53
+ count: { type: 'integer' }
54
+ }
55
+ },
56
+ updatedAt: { type: 'string' }
57
+ }
58
+ }
59
+
60
+ // Deterministic synthetic responses include Unicode and escaping, as API payloads do.
61
+ const address = {
62
+ street: '12 Example Street',
63
+ city: 'Łódź',
64
+ postalCode: '90-001',
65
+ country: 'PL',
66
+ apartment: null
67
+ }
68
+
69
+ const products = Array.from({ length: 50 }, (_, i) => ({
70
+ id: `product_${i + 1}`,
71
+ sku: `MUG-${1000 + i}`,
72
+ name: `Café mug "Everyday" — ${i + 1}`,
73
+ description: 'Handmade ceramic mug ☕\nDishwasher safe. Available in blue, green, and cream.',
74
+ price: {
75
+ amount: 1800 + i * 100,
76
+ currency: 'EUR',
77
+ discount: i % 2 === 0 ? null : 0.1
78
+ },
79
+ inventory: { available: i % 7 !== 0, quantity: i * 3, warehouse: 'eu-central' },
80
+ tags: ['home', 'kitchen', i % 2 === 0 ? 'ceramic' : 'stoneware'],
81
+ images: [
82
+ { url: `https://example.com/products/${i + 1}/front.jpg`, alt: 'Front view', primary: true },
83
+ { url: `https://example.com/products/${i + 1}/side.jpg`, alt: 'Side view', primary: false }
84
+ ],
85
+ rating: { average: 4.2 + (i % 5) / 10, count: 10 + i * 7 },
86
+ updatedAt: '2026-01-15T10:30:00.000Z'
87
+ }))
88
+
89
+ const orderItems = products.slice(0, 5).map((product, i) => ({
90
+ product,
91
+ quantity: i + 1,
92
+ unitPrice: product.price.amount,
93
+ total: product.price.amount * (i + 1)
94
+ }))
95
+ const subtotal = orderItems.reduce((sum, item) => sum + item.total, 0)
96
+
97
+ module.exports = [
98
+ {
99
+ name: 'user profile response',
100
+ schema: {
101
+ type: 'object',
102
+ properties: {
103
+ data: {
104
+ type: 'object',
105
+ properties: {
106
+ id: { type: 'string' },
107
+ email: { type: 'string' },
108
+ displayName: { type: 'string' },
109
+ bio: { type: 'string' },
110
+ avatarUrl: { type: ['string', 'null'] },
111
+ active: { type: 'boolean' },
112
+ roles: { type: 'array', items: { type: 'string' } },
113
+ address: addressSchema,
114
+ organization: {
115
+ type: 'object',
116
+ properties: {
117
+ id: { type: 'string' },
118
+ name: { type: 'string' },
119
+ plan: { type: 'string' }
120
+ }
121
+ },
122
+ preferences: {
123
+ type: 'object',
124
+ properties: {
125
+ locale: { type: 'string' },
126
+ timezone: { type: 'string' },
127
+ notifications: {
128
+ type: 'object',
129
+ properties: {
130
+ email: { type: 'boolean' },
131
+ push: { type: 'boolean' },
132
+ digest: { type: 'string' }
133
+ }
134
+ }
135
+ }
136
+ },
137
+ createdAt: { type: 'string' },
138
+ lastLoginAt: { type: ['string', 'null'] }
139
+ }
140
+ },
141
+ meta: {
142
+ type: 'object',
143
+ properties: { requestId: { type: 'string' }, version: { type: 'integer' } }
144
+ }
145
+ }
146
+ },
147
+ input: {
148
+ data: {
149
+ id: 'user_123',
150
+ email: 'alex@example.com',
151
+ displayName: 'Alex Żółć',
152
+ bio: 'Engineer & coffee enthusiast ☕\nWorking on "Project Atlas".',
153
+ avatarUrl: null,
154
+ active: true,
155
+ roles: ['member', 'billing-admin'],
156
+ address,
157
+ organization: { id: 'org_456', name: 'Example & Co.', plan: 'business' },
158
+ preferences: {
159
+ locale: 'pl-PL',
160
+ timezone: 'Europe/Warsaw',
161
+ notifications: { email: true, push: false, digest: 'weekly' }
162
+ },
163
+ createdAt: '2025-06-01T08:00:00.000Z',
164
+ lastLoginAt: '2026-01-15T10:30:00.000Z'
165
+ },
166
+ meta: { requestId: 'req_profile_123', version: 1 }
167
+ }
168
+ },
169
+ {
170
+ name: 'order response',
171
+ schema: {
172
+ type: 'object',
173
+ properties: {
174
+ id: { type: 'string' },
175
+ status: { type: 'string' },
176
+ customer: {
177
+ type: 'object',
178
+ properties: {
179
+ id: { type: 'string' },
180
+ name: { type: 'string' },
181
+ email: { type: 'string' }
182
+ }
183
+ },
184
+ shippingAddress: addressSchema,
185
+ items: {
186
+ type: 'array',
187
+ items: {
188
+ type: 'object',
189
+ properties: {
190
+ product: productSchema,
191
+ quantity: { type: 'integer' },
192
+ unitPrice: { type: 'integer' },
193
+ total: { type: 'integer' }
194
+ }
195
+ }
196
+ },
197
+ totals: {
198
+ type: 'object',
199
+ properties: {
200
+ subtotal: { type: 'integer' },
201
+ shipping: { type: 'integer' },
202
+ tax: { type: 'integer' },
203
+ total: { type: 'integer' },
204
+ currency: { type: 'string' }
205
+ }
206
+ },
207
+ payment: {
208
+ type: 'object',
209
+ properties: {
210
+ method: { type: 'string' },
211
+ paid: { type: 'boolean' },
212
+ transactionId: { type: ['string', 'null'] }
213
+ }
214
+ },
215
+ notes: { type: ['string', 'null'] },
216
+ createdAt: { type: 'string' }
217
+ }
218
+ },
219
+ input: {
220
+ id: 'order_789',
221
+ status: 'processing',
222
+ customer: { id: 'user_123', name: 'Alex Żółć', email: 'alex@example.com' },
223
+ shippingAddress: address,
224
+ items: orderItems,
225
+ totals: { subtotal, shipping: 500, tax: 4200, total: subtotal + 4700, currency: 'EUR' },
226
+ payment: { method: 'card', paid: true, transactionId: 'txn_987' },
227
+ notes: 'Please ring the bell marked "Office".\nLeave with reception if unavailable.',
228
+ createdAt: '2026-01-15T10:30:00.000Z'
229
+ }
230
+ },
231
+ {
232
+ name: 'paginated products response',
233
+ schema: {
234
+ type: 'object',
235
+ properties: {
236
+ data: { type: 'array', items: productSchema },
237
+ pagination: {
238
+ type: 'object',
239
+ properties: {
240
+ page: { type: 'integer' },
241
+ pageSize: { type: 'integer' },
242
+ total: { type: 'integer' },
243
+ hasNextPage: { type: 'boolean' }
244
+ }
245
+ },
246
+ links: {
247
+ type: 'object',
248
+ properties: {
249
+ self: { type: 'string' },
250
+ next: { type: 'string' },
251
+ previous: { type: ['string', 'null'] }
252
+ }
253
+ }
254
+ }
255
+ },
256
+ input: {
257
+ data: products,
258
+ pagination: { page: 1, pageSize: 50, total: 1248, hasNextPage: true },
259
+ links: {
260
+ self: '/products?page=1&pageSize=50',
261
+ next: '/products?page=2&pageSize=50',
262
+ previous: null
263
+ }
264
+ }
265
+ }
266
+ ]
package/index.js CHANGED
@@ -10,9 +10,6 @@ const Location = require('./lib/location')
10
10
  const validate = require('./lib/schema-validator')
11
11
  const mergeSchemas = require('./lib/merge-schemas')
12
12
 
13
- let largeArraySize = 2e4
14
- let largeArrayMechanism = 'default'
15
-
16
13
  const DEFAULT_MAX_DEPTH = 100
17
14
  const NAMED_FRAGMENT_REF = /^#[a-z_][-\w._]*$/i
18
15
 
@@ -345,6 +342,8 @@ function build (schema, options) {
345
342
  recursiveSchemas: new Set(),
346
343
  recursivePaths: new Set(),
347
344
  buildingSet: new Set(),
345
+ largeArraySize: 2e4,
346
+ largeArrayMechanism: 'default',
348
347
  uid: 0
349
348
  }
350
349
 
@@ -374,7 +373,7 @@ function build (schema, options) {
374
373
 
375
374
  if (options.largeArrayMechanism) {
376
375
  if (validLargeArrayMechanisms.has(options.largeArrayMechanism)) {
377
- largeArrayMechanism = options.largeArrayMechanism
376
+ context.largeArrayMechanism = options.largeArrayMechanism
378
377
  } else {
379
378
  throw new Error(`Unsupported large array mechanism ${options.largeArrayMechanism}`)
380
379
  }
@@ -385,11 +384,11 @@ function build (schema, options) {
385
384
  let parsedNumber
386
385
 
387
386
  if (largeArraySizeType === 'string' && Number.isFinite((parsedNumber = Number.parseInt(options.largeArraySize, 10)))) {
388
- largeArraySize = parsedNumber
387
+ context.largeArraySize = parsedNumber
389
388
  } else if (largeArraySizeType === 'number' && Number.isInteger(options.largeArraySize)) {
390
- largeArraySize = options.largeArraySize
389
+ context.largeArraySize = options.largeArraySize
391
390
  } else if (largeArraySizeType === 'bigint') {
392
- largeArraySize = Number(options.largeArraySize)
391
+ context.largeArraySize = Number(options.largeArraySize)
393
392
  } else {
394
393
  throw new Error(`Unsupported large array size. Expected integer-like, got ${typeof options.largeArraySize} with value ${options.largeArraySize}`)
395
394
  }
@@ -895,8 +894,8 @@ function buildArray (context, location, input) {
895
894
  `
896
895
  }
897
896
 
898
- if (largeArrayMechanism === 'json-stringify') {
899
- functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
897
+ if (context.largeArrayMechanism === 'json-stringify') {
898
+ functionCode += `if (arrayLength >= ${context.largeArraySize}) return JSON.stringify(obj)\n`
900
899
  }
901
900
 
902
901
  functionCode += `
@@ -980,8 +979,8 @@ function buildArray (context, location, input) {
980
979
  `
981
980
  }
982
981
 
983
- if (largeArrayMechanism === 'json-stringify') {
984
- inlinedCode += `if (arrayLength_${objVar} >= ${largeArraySize}) json += JSON.stringify(${objVar})\n else {`
982
+ if (context.largeArrayMechanism === 'json-stringify') {
983
+ inlinedCode += `if (arrayLength_${objVar} >= ${context.largeArraySize}) json += JSON.stringify(${objVar})\n else {`
985
984
  }
986
985
 
987
986
  inlinedCode += `
@@ -1038,7 +1037,7 @@ function buildArray (context, location, input) {
1038
1037
  json += JSON_STR_END_ARRAY
1039
1038
  `
1040
1039
 
1041
- if (largeArrayMechanism === 'json-stringify') {
1040
+ if (context.largeArrayMechanism === 'json-stringify') {
1042
1041
  inlinedCode += '}'
1043
1042
  }
1044
1043
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "7.1.0",
3
+ "version": "7.1.1",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -23,6 +23,31 @@ test('error on invalid largeArrayMechanism', (t) => {
23
23
  }), Error('Unsupported large array mechanism invalid'))
24
24
  })
25
25
 
26
+ test('does not leak largeArrayMechanism across build() calls', (t) => {
27
+ t.plan(2)
28
+
29
+ const users = []
30
+ for (let i = 0; i < 100; i++) users.push({ name: 'user' + i, password: 'secret' + i })
31
+
32
+ const schema = {
33
+ type: 'array',
34
+ items: {
35
+ type: 'object',
36
+ properties: {
37
+ name: { type: 'string' }
38
+ }
39
+ }
40
+ }
41
+
42
+ // An unrelated build() opts into json-stringify for its own schema.
43
+ const optedIn = build({ type: 'array' }, { largeArrayMechanism: 'json-stringify', largeArraySize: 100 })
44
+ t.assert.equal(optedIn(users).includes('password'), true)
45
+
46
+ // A later build() without options must not inherit the mechanism.
47
+ const stringify = build(schema)
48
+ t.assert.equal(stringify(users).includes('password'), false)
49
+ })
50
+
26
51
  function buildTest (schema, toStringify, options) {
27
52
  test(`render a ${schema.title} as JSON`, (t) => {
28
53
  t.plan(3)