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.
- package/benchmark/bench-cmp-lib.js +63 -38
- package/benchmark/bench-thread.js +4 -2
- package/benchmark/fixtures/api-responses.js +266 -0
- package/index.js +11 -12
- package/package.json +1 -1
- package/test/array.test.js +25 -0
|
@@ -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
|
-
|
|
177
|
-
|
|
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
package/test/array.test.js
CHANGED
|
@@ -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)
|