fast-json-stringify 6.2.0 → 6.4.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/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lock-threads.yml +19 -0
- package/LICENSE +1 -3
- package/README.md +4 -5
- package/benchmark/bench-cmp-lib.js +62 -46
- package/benchmark/bench-thread.js +3 -3
- package/index.js +112 -44
- package/package.json +12 -11
- package/test/anyof.test.js +20 -0
- package/test/array.test.js +50 -0
- package/test/debug-mode.test.js +21 -0
- package/test/if-then-else.test.js +72 -0
- package/test/invalidSchema.test.js +20 -0
- package/test/issue-836.test.js +183 -0
- package/test/json-add-comma.test.js +57 -57
- package/test/oneof.test.js +20 -0
- package/test/ref.test.js +31 -0
- package/test/typebox.test.js +1 -1
- package/types/{index.test-d.ts → index.tst.ts} +17 -17
- package/.github/stale.yml +0 -21
package/.github/workflows/ci.yml
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Lock Threads
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 0 1 * *'
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: lock
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lock-threads:
|
|
16
|
+
permissions:
|
|
17
|
+
issues: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
uses: fastify/workflows/.github/workflows/lock-threads.yml@v6
|
package/LICENSE
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2016-present Matteo Collina
|
|
4
|
-
Copyright (c) 2016-present The Fastify team
|
|
5
|
-
|
|
6
|
-
The Fastify team members are listed at https://github.com/fastify/fastify#team.
|
|
4
|
+
Copyright (c) 2016-present The Fastify team <https://github.com/fastify/fastify#team>
|
|
7
5
|
|
|
8
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
7
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -79,7 +79,6 @@ compile-json-stringify date format x 803,214 ops/sec ±0.23% (97 runs sampled)
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
<a name="example"></a>
|
|
82
|
-
Try it out on RunKit: <a href="https://runkit.com/npm/fast-json-stringify">https://runkit.com/npm/fast-json-stringify</a>
|
|
83
82
|
## Example
|
|
84
83
|
|
|
85
84
|
```js
|
|
@@ -347,9 +346,9 @@ const stringify = fastJson({
|
|
|
347
346
|
properties: {
|
|
348
347
|
'undecidedType': {
|
|
349
348
|
'anyOf': [{
|
|
350
|
-
|
|
349
|
+
type: 'string'
|
|
351
350
|
}, {
|
|
352
|
-
|
|
351
|
+
type: 'boolean'
|
|
353
352
|
}]
|
|
354
353
|
}
|
|
355
354
|
}
|
|
@@ -645,8 +644,8 @@ const stringify = fastJson({
|
|
|
645
644
|
type: 'object',
|
|
646
645
|
properties: {
|
|
647
646
|
'code': {
|
|
648
|
-
|
|
649
|
-
|
|
647
|
+
type: 'string',
|
|
648
|
+
format: 'unsafe'
|
|
650
649
|
}
|
|
651
650
|
}
|
|
652
651
|
})
|
|
@@ -84,13 +84,13 @@ const arraySchemaAJVJTD = {
|
|
|
84
84
|
const dateFormatSchema = {
|
|
85
85
|
description: 'Date of birth',
|
|
86
86
|
type: 'string',
|
|
87
|
-
format: '
|
|
87
|
+
format: 'datetime'
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const dateFormatSchemaCJS = {
|
|
91
91
|
description: 'Date of birth',
|
|
92
92
|
type: 'string',
|
|
93
|
-
format: '
|
|
93
|
+
format: 'datetime'
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
const obj = {
|
|
@@ -169,155 +169,171 @@ for (let i = STR_LEN; i < LARGE_ARRAY_SIZE; ++i) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
Number(str)
|
|
173
|
-
|
|
174
172
|
for (let i = 0; i < MULTI_ARRAY_LENGTH; i++) {
|
|
175
173
|
multiArray[i] = obj
|
|
176
174
|
}
|
|
177
175
|
|
|
178
|
-
suite.add('
|
|
176
|
+
suite.add('fast-json-stringify: creation', function () {
|
|
179
177
|
FJS(schema)
|
|
180
178
|
})
|
|
181
|
-
suite.add('
|
|
179
|
+
suite.add('compile-json-stringify: creation', function () {
|
|
182
180
|
CJS(schemaCJS)
|
|
183
181
|
})
|
|
184
|
-
suite.add('AJV
|
|
182
|
+
suite.add('AJV: creation', function () {
|
|
185
183
|
ajv.compileSerializer(schemaAJVJTD)
|
|
186
184
|
})
|
|
187
|
-
suite.add('json-accelerator creation', function () {
|
|
185
|
+
suite.add('json-accelerator: creation', function () {
|
|
188
186
|
createAccelerator(schema)
|
|
189
187
|
})
|
|
190
188
|
|
|
191
|
-
suite.add('JSON.stringify array', function () {
|
|
189
|
+
suite.add('JSON.stringify: array', function () {
|
|
192
190
|
JSON.stringify(multiArray)
|
|
193
191
|
})
|
|
194
192
|
|
|
195
|
-
suite.add('fast-json-stringify array
|
|
193
|
+
suite.add('fast-json-stringify [default]: array', function () {
|
|
196
194
|
stringifyArrayDefault(multiArray)
|
|
197
195
|
})
|
|
198
196
|
|
|
199
|
-
suite.add('json-accelerator array', function () {
|
|
197
|
+
suite.add('json-accelerator: array', function () {
|
|
200
198
|
accelArray(multiArray)
|
|
201
199
|
})
|
|
202
200
|
|
|
203
|
-
suite.add('fast-json-stringify
|
|
201
|
+
suite.add('fast-json-stringify [json-stringify]: array', function () {
|
|
204
202
|
stringifyArrayJSONStringify(multiArray)
|
|
205
203
|
})
|
|
206
204
|
|
|
207
|
-
suite.add('compile-json-stringify array', function () {
|
|
205
|
+
suite.add('compile-json-stringify: array', function () {
|
|
208
206
|
CJSStringifyArray(multiArray)
|
|
209
207
|
})
|
|
210
208
|
|
|
211
|
-
suite.add('AJV
|
|
209
|
+
suite.add('AJV: array', function () {
|
|
212
210
|
ajvSerializeArray(multiArray)
|
|
213
211
|
})
|
|
214
212
|
|
|
215
|
-
suite.add('JSON.stringify large array', function () {
|
|
213
|
+
suite.add('JSON.stringify: large array', function () {
|
|
216
214
|
JSON.stringify(largeArray)
|
|
217
215
|
})
|
|
218
216
|
|
|
219
|
-
suite.add('fast-json-stringify large array
|
|
217
|
+
suite.add('fast-json-stringify [default]: large array', function () {
|
|
220
218
|
stringifyArrayDefault(largeArray)
|
|
221
219
|
})
|
|
222
220
|
|
|
223
|
-
suite.add('fast-json-stringify large array
|
|
221
|
+
suite.add('fast-json-stringify [json-stringify]: large array', function () {
|
|
224
222
|
stringifyArrayJSONStringify(largeArray)
|
|
225
223
|
})
|
|
226
224
|
|
|
227
|
-
suite.add('compile-json-stringify large array', function () {
|
|
225
|
+
suite.add('compile-json-stringify: large array', function () {
|
|
228
226
|
CJSStringifyArray(largeArray)
|
|
229
227
|
})
|
|
230
228
|
|
|
231
|
-
suite.add('AJV
|
|
229
|
+
suite.add('AJV: large array', function () {
|
|
232
230
|
ajvSerializeArray(largeArray)
|
|
233
231
|
})
|
|
234
232
|
|
|
235
|
-
suite.add('JSON.stringify long string', function () {
|
|
233
|
+
suite.add('JSON.stringify: long string', function () {
|
|
236
234
|
JSON.stringify(str)
|
|
237
235
|
})
|
|
238
236
|
|
|
239
|
-
suite.add('fast-json-stringify long string', function () {
|
|
237
|
+
suite.add('fast-json-stringify: long string', function () {
|
|
240
238
|
stringifyString(str)
|
|
241
239
|
})
|
|
242
240
|
|
|
243
|
-
suite.add('json-accelerator long string', function () {
|
|
241
|
+
suite.add('json-accelerator: long string', function () {
|
|
244
242
|
stringifyString(str)
|
|
245
243
|
})
|
|
246
244
|
|
|
247
|
-
suite.add('compile-json-stringify long string', function () {
|
|
245
|
+
suite.add('compile-json-stringify: long string', function () {
|
|
248
246
|
CJSStringifyString(str)
|
|
249
247
|
})
|
|
250
248
|
|
|
251
|
-
suite.add('AJV
|
|
249
|
+
suite.add('AJV: long string', function () {
|
|
252
250
|
ajvSerializeString(str)
|
|
253
251
|
})
|
|
254
252
|
|
|
255
|
-
suite.add('JSON.stringify short string', function () {
|
|
253
|
+
suite.add('JSON.stringify: short string', function () {
|
|
256
254
|
JSON.stringify('hello world')
|
|
257
255
|
})
|
|
258
256
|
|
|
259
|
-
suite.add('fast-json-stringify short string', function () {
|
|
257
|
+
suite.add('fast-json-stringify: short string', function () {
|
|
260
258
|
stringifyString('hello world')
|
|
261
259
|
})
|
|
262
260
|
|
|
263
|
-
suite.add('json-accelerator short string', function () {
|
|
261
|
+
suite.add('json-accelerator: short string', function () {
|
|
264
262
|
accelString('hello world')
|
|
265
263
|
})
|
|
266
264
|
|
|
267
|
-
suite.add('compile-json-stringify short string', function () {
|
|
265
|
+
suite.add('compile-json-stringify: short string', function () {
|
|
268
266
|
CJSStringifyString('hello world')
|
|
269
267
|
})
|
|
270
268
|
|
|
271
|
-
suite.add('AJV
|
|
269
|
+
suite.add('AJV: short string', function () {
|
|
272
270
|
ajvSerializeString('hello world')
|
|
273
271
|
})
|
|
274
272
|
|
|
275
|
-
suite.add('JSON.stringify obj', function () {
|
|
273
|
+
suite.add('JSON.stringify: obj', function () {
|
|
276
274
|
JSON.stringify(obj)
|
|
277
275
|
})
|
|
278
276
|
|
|
279
|
-
suite.add('fast-json-stringify obj', function () {
|
|
277
|
+
suite.add('fast-json-stringify: obj', function () {
|
|
280
278
|
stringify(obj)
|
|
281
279
|
})
|
|
282
280
|
|
|
283
|
-
suite.add('json-accelerator obj', function () {
|
|
281
|
+
suite.add('json-accelerator: obj', function () {
|
|
284
282
|
accelStringify(obj)
|
|
285
283
|
})
|
|
286
284
|
|
|
287
|
-
suite.add('compile-json-stringify obj', function () {
|
|
285
|
+
suite.add('compile-json-stringify: obj', function () {
|
|
288
286
|
CJSStringify(obj)
|
|
289
287
|
})
|
|
290
288
|
|
|
291
|
-
suite.add('AJV
|
|
289
|
+
suite.add('AJV: obj', function () {
|
|
292
290
|
ajvSerialize(obj)
|
|
293
291
|
})
|
|
294
292
|
|
|
295
|
-
suite.add('JSON
|
|
293
|
+
suite.add('JSON.stringify: date', function () {
|
|
296
294
|
JSON.stringify(date)
|
|
297
295
|
})
|
|
298
296
|
|
|
299
|
-
suite.add('fast-json-stringify date
|
|
297
|
+
suite.add('fast-json-stringify: date', function () {
|
|
300
298
|
stringifyDate(date)
|
|
301
299
|
})
|
|
302
300
|
|
|
303
|
-
suite.add('json-accelerate date
|
|
301
|
+
suite.add('json-accelerate: date', function () {
|
|
304
302
|
accelDate(date)
|
|
305
303
|
})
|
|
306
304
|
|
|
307
|
-
suite.add('compile-json-stringify date
|
|
305
|
+
suite.add('compile-json-stringify: date', function () {
|
|
308
306
|
CJSStringifyDate(date)
|
|
309
307
|
})
|
|
310
308
|
|
|
311
309
|
suite.run().then(() => {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
const results = suite.tasks.map(task => ({
|
|
311
|
+
name: task.name,
|
|
312
|
+
hz: task.result.throughput.mean,
|
|
313
|
+
rme: task.result.latency.rme,
|
|
314
|
+
samples: task.result.latency.df + 1
|
|
315
|
+
}))
|
|
316
|
+
|
|
317
|
+
const scenarios = {}
|
|
318
|
+
for (const result of results) {
|
|
319
|
+
const [library, scenario] = result.name.split(':').map(s => s.trim())
|
|
320
|
+
|
|
321
|
+
if (!scenarios[scenario]) scenarios[scenario] = []
|
|
322
|
+
scenarios[scenario].push({ ...result, library })
|
|
323
|
+
}
|
|
316
324
|
|
|
317
|
-
|
|
318
|
-
|
|
325
|
+
for (const [scenario, tasks] of Object.entries(scenarios)) {
|
|
326
|
+
console.log(`\n--- ${scenario} ---`)
|
|
327
|
+
const sorted = tasks.sort((a, b) => b.hz - a.hz)
|
|
328
|
+
const winner = sorted[0]
|
|
319
329
|
|
|
320
|
-
const
|
|
321
|
-
|
|
330
|
+
for (const task of sorted) {
|
|
331
|
+
const formattedHz = task.hz.toLocaleString('en-US', { maximumFractionDigits: 0 })
|
|
332
|
+
const formattedRme = task.rme.toFixed(2)
|
|
333
|
+
const isWinner = task === winner
|
|
334
|
+
const prefix = isWinner ? '🏆 ' : ' '
|
|
335
|
+
|
|
336
|
+
console.log(`${prefix}${task.library.padEnd(40)} x ${formattedHz.padStart(15)} ops/sec ±${formattedRme}% (${task.samples} runs sampled)`)
|
|
337
|
+
}
|
|
322
338
|
}
|
|
323
339
|
}).catch(err => console.error(`Error: ${err.message}`))
|
|
@@ -21,9 +21,9 @@ bench.add(benchmark.name, () => {
|
|
|
21
21
|
stringify(benchmark.input)
|
|
22
22
|
}).run().then(() => {
|
|
23
23
|
const task = bench.tasks[0]
|
|
24
|
-
const hz = task.result.
|
|
25
|
-
const rme = task.result.rme // relative margin of error (%)
|
|
26
|
-
const samples = task.result.
|
|
24
|
+
const hz = task.result.throughput.mean // ops/sec
|
|
25
|
+
const rme = task.result.latency.rme // relative margin of error (%)
|
|
26
|
+
const samples = task.result.latency.df + 1 // degrees of freedom + 1 = sample count
|
|
27
27
|
|
|
28
28
|
const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 })
|
|
29
29
|
const formattedRme = rme.toFixed(2)
|
package/index.js
CHANGED
|
@@ -304,14 +304,14 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma, objV
|
|
|
304
304
|
const propertiesKeys = Object.keys(schema.properties || {})
|
|
305
305
|
|
|
306
306
|
let code = `
|
|
307
|
-
const
|
|
308
|
-
for (const [key, value] of Object.entries(${objVar})) {
|
|
307
|
+
for (const key of Object.keys(${objVar})) {
|
|
309
308
|
if (
|
|
310
|
-
propertiesKeys.
|
|
311
|
-
|
|
312
|
-
typeof
|
|
313
|
-
typeof
|
|
309
|
+
${propertiesKeys.length > 0 ? propertiesKeys.map(k => `key === ${JSON.stringify(k)}`).join(' || ') + ' ||' : ''}
|
|
310
|
+
${objVar}[key] === undefined ||
|
|
311
|
+
typeof ${objVar}[key] === 'function' ||
|
|
312
|
+
typeof ${objVar}[key] === 'symbol'
|
|
314
313
|
) continue
|
|
314
|
+
const value = ${objVar}[key]
|
|
315
315
|
`
|
|
316
316
|
|
|
317
317
|
const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
|
|
@@ -385,45 +385,99 @@ function buildInnerObject (context, location, objVar) {
|
|
|
385
385
|
|
|
386
386
|
const localUid = context.uid++
|
|
387
387
|
let addComma = ''
|
|
388
|
-
const needsRuntimeComma = propertiesKeys.length > 1 || schema.patternProperties || (schema.additionalProperties !== undefined && schema.additionalProperties !== false)
|
|
389
388
|
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
389
|
+
if (requiredProperties.length > 0) {
|
|
390
|
+
// If we have required properties, we know that at least one property will be serialized.
|
|
391
|
+
// We can avoid the runtime check for the comma.
|
|
394
392
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (propertyLocation.schema.$ref) {
|
|
398
|
-
propertyLocation = resolveRef(context, propertyLocation)
|
|
399
|
-
}
|
|
393
|
+
// The first property is required, so we don't need a comma.
|
|
394
|
+
// For the subsequent properties, we can blindly add a comma.
|
|
400
395
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const isRequired = requiredProperties.includes(key)
|
|
396
|
+
for (let i = 0; i < propertiesKeys.length; i++) {
|
|
397
|
+
const key = propertiesKeys[i]
|
|
398
|
+
const propertyLocation = propertiesLocation.getPropertyLocation(key)
|
|
405
399
|
|
|
406
|
-
|
|
400
|
+
let resolvedLocation = propertyLocation
|
|
401
|
+
if (propertyLocation.schema.$ref) {
|
|
402
|
+
resolvedLocation = resolveRef(context, propertyLocation)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const sanitizedKey = JSON.stringify(key)
|
|
406
|
+
const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
|
|
407
|
+
const defaultValue = resolvedLocation.schema.default
|
|
408
|
+
const isRequired = requiredProperties.includes(key)
|
|
409
|
+
|
|
410
|
+
// i === 0 means it's the first property, and it IS required (due to sort). No comma.
|
|
411
|
+
// i > 0 means it follows a required property (or a sequence starting with one). Unconditional comma.
|
|
412
|
+
const currentAddComma = i === 0 ? '' : 'json += JSON_STR_COMMA'
|
|
413
|
+
|
|
414
|
+
code += `
|
|
407
415
|
const ${value} = ${objVar}[${sanitizedKey}]
|
|
408
416
|
if (${value} !== undefined) {
|
|
409
|
-
${
|
|
417
|
+
${currentAddComma}
|
|
410
418
|
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
411
|
-
${buildValue(context,
|
|
419
|
+
${buildValue(context, resolvedLocation, `${value}`)}
|
|
412
420
|
}`
|
|
413
421
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
${
|
|
422
|
+
if (defaultValue !== undefined) {
|
|
423
|
+
code += ` else {
|
|
424
|
+
${currentAddComma}
|
|
417
425
|
json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
|
|
418
426
|
}
|
|
419
427
|
`
|
|
420
|
-
|
|
421
|
-
|
|
428
|
+
} else if (isRequired) {
|
|
429
|
+
code += ` else {
|
|
422
430
|
throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')
|
|
423
431
|
}
|
|
424
432
|
`
|
|
425
|
-
|
|
426
|
-
|
|
433
|
+
} else {
|
|
434
|
+
code += '\n'
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
addComma = 'json += JSON_STR_COMMA'
|
|
439
|
+
} else {
|
|
440
|
+
const needsRuntimeComma = propertiesKeys.length > 1 || schema.patternProperties || (schema.additionalProperties !== undefined && schema.additionalProperties !== false)
|
|
441
|
+
|
|
442
|
+
if (needsRuntimeComma) {
|
|
443
|
+
code += `let addComma_${localUid} = false\n`
|
|
444
|
+
addComma = `!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)`
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for (const key of propertiesKeys) {
|
|
448
|
+
let propertyLocation = propertiesLocation.getPropertyLocation(key)
|
|
449
|
+
if (propertyLocation.schema.$ref) {
|
|
450
|
+
propertyLocation = resolveRef(context, propertyLocation)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const sanitizedKey = JSON.stringify(key)
|
|
454
|
+
const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
|
|
455
|
+
const defaultValue = propertyLocation.schema.default
|
|
456
|
+
const isRequired = requiredProperties.includes(key) // Should be false here but good to keep
|
|
457
|
+
|
|
458
|
+
code += `
|
|
459
|
+
const ${value} = ${objVar}[${sanitizedKey}]
|
|
460
|
+
if (${value} !== undefined) {
|
|
461
|
+
${addComma}
|
|
462
|
+
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
463
|
+
${buildValue(context, propertyLocation, `${value}`)}
|
|
464
|
+
}`
|
|
465
|
+
|
|
466
|
+
if (defaultValue !== undefined) {
|
|
467
|
+
code += ` else {
|
|
468
|
+
${addComma}
|
|
469
|
+
json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
|
|
470
|
+
}
|
|
471
|
+
`
|
|
472
|
+
} else if (isRequired) {
|
|
473
|
+
// Should not happen if requiredProperties.length === 0 but safety
|
|
474
|
+
code += ` else {
|
|
475
|
+
throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')
|
|
476
|
+
}
|
|
477
|
+
`
|
|
478
|
+
} else {
|
|
479
|
+
code += '\n'
|
|
480
|
+
}
|
|
427
481
|
}
|
|
428
482
|
}
|
|
429
483
|
|
|
@@ -514,7 +568,7 @@ function buildObject (context, location, input) {
|
|
|
514
568
|
const jsonPointer = location.jsonPointer || ''
|
|
515
569
|
const fullPath = `${schemaId}#${jsonPointer}`
|
|
516
570
|
|
|
517
|
-
if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
|
|
571
|
+
if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema) || schemaId !== '') {
|
|
518
572
|
const functionName = generateFuncName(context)
|
|
519
573
|
context.functionsNamesBySchema.set(schema, functionName)
|
|
520
574
|
|
|
@@ -573,7 +627,7 @@ function buildArray (context, location, input) {
|
|
|
573
627
|
const jsonPointer = location.jsonPointer || ''
|
|
574
628
|
const fullPath = `${schemaId}#${jsonPointer}`
|
|
575
629
|
|
|
576
|
-
if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
|
|
630
|
+
if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema) || schemaId !== '') {
|
|
577
631
|
const functionName = generateFuncName(context)
|
|
578
632
|
context.functionsNamesBySchema.set(schema, functionName)
|
|
579
633
|
|
|
@@ -611,10 +665,15 @@ function buildArray (context, location, input) {
|
|
|
611
665
|
|
|
612
666
|
if (Array.isArray(itemsSchema)) {
|
|
613
667
|
for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) {
|
|
614
|
-
|
|
668
|
+
let item = itemsSchema[i]
|
|
669
|
+
let itemLocation = itemsLocation.getPropertyLocation(i)
|
|
670
|
+
if (itemLocation.schema.$ref) {
|
|
671
|
+
itemLocation = resolveRef(context, itemLocation)
|
|
672
|
+
item = itemLocation.schema
|
|
673
|
+
}
|
|
615
674
|
const value = `value_${i}`
|
|
616
675
|
functionCode += `const ${value} = obj[${i}]`
|
|
617
|
-
const tmpRes = buildValue(context,
|
|
676
|
+
const tmpRes = buildValue(context, itemLocation, value)
|
|
618
677
|
functionCode += `
|
|
619
678
|
if (${i} < arrayLength) {
|
|
620
679
|
if (${buildArrayTypeCondition(item.type, value)}) {
|
|
@@ -641,12 +700,14 @@ function buildArray (context, location, input) {
|
|
|
641
700
|
} else {
|
|
642
701
|
const code = buildValue(context, itemsLocation, 'value')
|
|
643
702
|
functionCode += `
|
|
644
|
-
|
|
645
|
-
|
|
703
|
+
if (arrayLength > 0) {
|
|
704
|
+
const value = obj[0]
|
|
705
|
+
${code}
|
|
706
|
+
for (let i = 1; i < arrayLength; i++) {
|
|
646
707
|
json += JSON_STR_COMMA
|
|
708
|
+
const value = obj[i]
|
|
709
|
+
${code}
|
|
647
710
|
}
|
|
648
|
-
const value = obj[i]
|
|
649
|
-
${code}
|
|
650
711
|
}`
|
|
651
712
|
}
|
|
652
713
|
|
|
@@ -691,10 +752,15 @@ function buildArray (context, location, input) {
|
|
|
691
752
|
const localUid = context.uid++
|
|
692
753
|
inlinedCode += `let addComma_${localUid} = false\n`
|
|
693
754
|
for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) {
|
|
694
|
-
|
|
755
|
+
let item = itemsSchema[i]
|
|
756
|
+
let itemLocation = itemsLocation.getPropertyLocation(i)
|
|
757
|
+
if (itemLocation.schema.$ref) {
|
|
758
|
+
itemLocation = resolveRef(context, itemLocation)
|
|
759
|
+
item = itemLocation.schema
|
|
760
|
+
}
|
|
695
761
|
const value = `value_${i}_${context.uid++}`
|
|
696
762
|
inlinedCode += `const ${value} = ${objVar}[${i}]`
|
|
697
|
-
const tmpRes = buildValue(context,
|
|
763
|
+
const tmpRes = buildValue(context, itemLocation, value)
|
|
698
764
|
inlinedCode += `
|
|
699
765
|
if (${i} < arrayLength_${objVar}) {
|
|
700
766
|
if (${buildArrayTypeCondition(item.type, value)}) {
|
|
@@ -717,12 +783,14 @@ function buildArray (context, location, input) {
|
|
|
717
783
|
} else {
|
|
718
784
|
const code = buildValue(context, itemsLocation, 'value')
|
|
719
785
|
inlinedCode += `
|
|
720
|
-
|
|
721
|
-
|
|
786
|
+
if (arrayLength_${objVar} > 0) {
|
|
787
|
+
const value = ${objVar}[0]
|
|
788
|
+
${code}
|
|
789
|
+
for (let i = 1; i < arrayLength_${objVar}; i++) {
|
|
722
790
|
json += JSON_STR_COMMA
|
|
791
|
+
const value = ${objVar}[i]
|
|
792
|
+
${code}
|
|
723
793
|
}
|
|
724
|
-
const value = ${objVar}[i]
|
|
725
|
-
${code}
|
|
726
794
|
}`
|
|
727
795
|
}
|
|
728
796
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-json-stringify",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"description": "Stringify your JSON at max speed",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"benchmark": "node --expose-gc ./benchmark/bench-cmp-lib.js",
|
|
13
13
|
"lint": "eslint",
|
|
14
14
|
"lint:fix": "eslint --fix",
|
|
15
|
-
"test:typescript": "
|
|
15
|
+
"test:typescript": "tstyche",
|
|
16
16
|
"test:unit": "c8 node --test",
|
|
17
17
|
"test": "npm run test:unit && npm run test:typescript"
|
|
18
18
|
},
|
|
@@ -62,19 +62,20 @@
|
|
|
62
62
|
}
|
|
63
63
|
],
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"
|
|
66
|
-
"c8": "^10.1.2",
|
|
65
|
+
"c8": "^11.0.0",
|
|
67
66
|
"cli-select": "^1.1.2",
|
|
68
67
|
"compile-json-stringify": "^0.1.2",
|
|
69
|
-
"eslint": "^9.
|
|
68
|
+
"eslint": "^9.39.2",
|
|
70
69
|
"fast-json-stringify": ".",
|
|
71
70
|
"is-my-json-valid": "^2.20.6",
|
|
72
|
-
"json-accelerator": "^0.
|
|
73
|
-
"neostandard": "^0.
|
|
74
|
-
"simple-git": "^3.
|
|
75
|
-
"tinybench": "^
|
|
76
|
-
"
|
|
77
|
-
"
|
|
71
|
+
"json-accelerator": "^0.1.7",
|
|
72
|
+
"neostandard": "^0.13.0",
|
|
73
|
+
"simple-git": "^3.30.0",
|
|
74
|
+
"tinybench": "^6.0.0",
|
|
75
|
+
"tstyche": "^7.1.0",
|
|
76
|
+
"typebox": "^1.0.81",
|
|
77
|
+
"typescript": "~6.0.2",
|
|
78
|
+
"webpack": "^5.104.1"
|
|
78
79
|
},
|
|
79
80
|
"dependencies": {
|
|
80
81
|
"@fastify/merge-json-schemas": "^0.2.0",
|
package/test/anyof.test.js
CHANGED
|
@@ -790,3 +790,23 @@ test('should build merged schemas twice', (t) => {
|
|
|
790
790
|
t.assert.equal(stringify({ enums: 'BAR' }), '{"enums":"BAR"}')
|
|
791
791
|
}
|
|
792
792
|
})
|
|
793
|
+
|
|
794
|
+
test('invalid anyOf schema', (t) => {
|
|
795
|
+
t.plan(1)
|
|
796
|
+
|
|
797
|
+
const schema = {
|
|
798
|
+
type: 'object',
|
|
799
|
+
properties: {
|
|
800
|
+
prop: {
|
|
801
|
+
anyOf: 'not array' // invalid, anyOf must be array
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
try {
|
|
807
|
+
build(schema)
|
|
808
|
+
t.assert.fail('Should throw')
|
|
809
|
+
} catch (err) {
|
|
810
|
+
t.assert.ok(err.message.includes('schema is invalid'))
|
|
811
|
+
}
|
|
812
|
+
})
|
package/test/array.test.js
CHANGED
|
@@ -61,6 +61,34 @@ buildTest({
|
|
|
61
61
|
dates: [new Date(1), new Date(2)]
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
+
buildTest({
|
|
65
|
+
title: 'dates tuple $ref',
|
|
66
|
+
definitions: {
|
|
67
|
+
dateTime: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
format: 'date-time'
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
dates: {
|
|
75
|
+
type: 'array',
|
|
76
|
+
minItems: 2,
|
|
77
|
+
maxItems: 2,
|
|
78
|
+
items: [
|
|
79
|
+
{
|
|
80
|
+
$ref: '#/definitions/dateTime'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
$ref: '#/definitions/dateTime'
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}, {
|
|
89
|
+
dates: [new Date(1), new Date(2)]
|
|
90
|
+
})
|
|
91
|
+
|
|
64
92
|
buildTest({
|
|
65
93
|
title: 'string array',
|
|
66
94
|
type: 'object',
|
|
@@ -358,6 +386,28 @@ test('array items is a schema and additionalItems is false', (t) => {
|
|
|
358
386
|
t.assert.equal(validate({ foo: ['foo', 'bar'] }), true)
|
|
359
387
|
})
|
|
360
388
|
|
|
389
|
+
test('array items is a list of schema and additionalItems is a schema', (t) => {
|
|
390
|
+
t.plan(1)
|
|
391
|
+
|
|
392
|
+
const schema = {
|
|
393
|
+
type: 'object',
|
|
394
|
+
properties: {
|
|
395
|
+
foo: {
|
|
396
|
+
type: 'array',
|
|
397
|
+
items: [
|
|
398
|
+
{ type: 'string' }
|
|
399
|
+
],
|
|
400
|
+
additionalItems: { type: 'number' }
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const stringify = build(schema)
|
|
406
|
+
const result = stringify({ foo: ['foo', 42] })
|
|
407
|
+
|
|
408
|
+
t.assert.equal(result, '{"foo":["foo",42]}')
|
|
409
|
+
})
|
|
410
|
+
|
|
361
411
|
// https://github.com/fastify/fast-json-stringify/issues/279
|
|
362
412
|
test('object array with anyOf and symbol', (t) => {
|
|
363
413
|
t.plan(1)
|
package/test/debug-mode.test.js
CHANGED
|
@@ -119,3 +119,24 @@ test('debug should restore the same serializer instance', t => {
|
|
|
119
119
|
const compiled = fjs.restore(debugMode)
|
|
120
120
|
t.assert.equal(compiled(3.95), 4)
|
|
121
121
|
})
|
|
122
|
+
|
|
123
|
+
test('Serializer restoreFromState', t => {
|
|
124
|
+
t.plan(1)
|
|
125
|
+
|
|
126
|
+
const Serializer = require('../lib/serializer')
|
|
127
|
+
const serializer = new Serializer()
|
|
128
|
+
const state = serializer.getState()
|
|
129
|
+
const restored = Serializer.restoreFromState(state)
|
|
130
|
+
t.assert.ok(restored instanceof Serializer)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('Validator restoreFromState', t => {
|
|
134
|
+
t.plan(1)
|
|
135
|
+
|
|
136
|
+
const Validator = require('../lib/validator')
|
|
137
|
+
const validator = new Validator()
|
|
138
|
+
validator.addSchema({ type: 'string' }, 'test')
|
|
139
|
+
const state = validator.getState()
|
|
140
|
+
const restored = Validator.restoreFromState(state)
|
|
141
|
+
t.assert.ok(restored instanceof Validator)
|
|
142
|
+
})
|
|
@@ -466,3 +466,75 @@ test('external recursive if/then/else', (t) => {
|
|
|
466
466
|
const stringify = build(schema, { schema: { externalSchema } })
|
|
467
467
|
t.assert.equal(stringify(data), '{"a":{"base":"a","bar":"42"},"b":{"base":"b","baz":"43"}}')
|
|
468
468
|
})
|
|
469
|
+
|
|
470
|
+
test('invalid if-then-else schema', (t) => {
|
|
471
|
+
t.plan(1)
|
|
472
|
+
|
|
473
|
+
const schema = {
|
|
474
|
+
type: 'object',
|
|
475
|
+
if: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
kind: { type: 'string', enum: ['foobar'] }
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
then: {
|
|
482
|
+
type: 'object',
|
|
483
|
+
properties: {
|
|
484
|
+
foo: { type: 'string' }
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
else: {
|
|
488
|
+
type: 'object',
|
|
489
|
+
properties: 'invalid' // properties must be object
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
build(schema)
|
|
495
|
+
t.assert.fail('Should throw')
|
|
496
|
+
} catch (err) {
|
|
497
|
+
t.assert.ok(err.message.includes('schema is invalid'))
|
|
498
|
+
}
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
test('if-then-else with allOf', (t) => {
|
|
502
|
+
t.plan(2)
|
|
503
|
+
|
|
504
|
+
const schema = {
|
|
505
|
+
type: 'object',
|
|
506
|
+
allOf: [
|
|
507
|
+
{
|
|
508
|
+
type: 'object',
|
|
509
|
+
properties: {
|
|
510
|
+
base: { type: 'string' }
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
],
|
|
514
|
+
if: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {
|
|
517
|
+
kind: { type: 'string', enum: ['foobar'] }
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
then: {
|
|
521
|
+
type: 'object',
|
|
522
|
+
properties: {
|
|
523
|
+
foo: { type: 'string' }
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
else: {
|
|
527
|
+
type: 'object',
|
|
528
|
+
properties: {
|
|
529
|
+
bar: { type: 'string' }
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const stringify = build(schema)
|
|
535
|
+
const data = { base: 'test', kind: 'foobar', foo: 'value' }
|
|
536
|
+
const output = stringify(data)
|
|
537
|
+
|
|
538
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
539
|
+
t.assert.equal(output, '{"base":"test","foo":"value"}')
|
|
540
|
+
})
|
|
@@ -16,3 +16,23 @@ test('Should throw on invalid schema', t => {
|
|
|
16
16
|
})
|
|
17
17
|
}, { message: /^"invalid" schema is invalid:.*/ })
|
|
18
18
|
})
|
|
19
|
+
|
|
20
|
+
test('invalid not schema', (t) => {
|
|
21
|
+
t.plan(1)
|
|
22
|
+
|
|
23
|
+
const schema = {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
prop: {
|
|
27
|
+
not: 'not object' // invalid, not must be object
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
build(schema)
|
|
34
|
+
t.assert.fail('Should throw')
|
|
35
|
+
} catch (err) {
|
|
36
|
+
t.assert.ok(err.message.includes('schema is invalid'))
|
|
37
|
+
}
|
|
38
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
test('external $ref schema should be reused, not inlined at every reference', (t) => {
|
|
7
|
+
t.plan(3)
|
|
8
|
+
|
|
9
|
+
const contactSchema = {
|
|
10
|
+
$id: 'contact.json',
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
firstName: { type: 'string' },
|
|
14
|
+
lastName: { type: 'string' },
|
|
15
|
+
email: { type: 'string' }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Schema referencing the same external $ref multiple times
|
|
20
|
+
const parentSchema = {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
owner: { $ref: 'contact.json' },
|
|
24
|
+
assignee: { $ref: 'contact.json' },
|
|
25
|
+
reporter: { $ref: 'contact.json' }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const serializer = build(parentSchema, { schema: { 'contact.json': contactSchema } })
|
|
30
|
+
const code = serializer.toString()
|
|
31
|
+
|
|
32
|
+
// The serialized function should contain extracted anonymous functions
|
|
33
|
+
// rather than inlining the full contact schema at every reference.
|
|
34
|
+
// With the fix, `firstName` appears 0 times in the main serializer body
|
|
35
|
+
// (it is inside the extracted function, which is not included in .toString())
|
|
36
|
+
// or at most 1 time if in a single extracted function.
|
|
37
|
+
const firstNameMatches = code.match(/firstName/g)
|
|
38
|
+
t.assert.ok(firstNameMatches === null || firstNameMatches.length <= 1,
|
|
39
|
+
`firstName should appear at most once (extracted function), got ${firstNameMatches ? firstNameMatches.length : 0}`)
|
|
40
|
+
|
|
41
|
+
// Verify correct serialization
|
|
42
|
+
const data = {
|
|
43
|
+
owner: { firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
|
|
44
|
+
assignee: { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' },
|
|
45
|
+
reporter: { firstName: 'Bob', lastName: 'Jones', email: 'bob@example.com' }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const output = serializer(data)
|
|
49
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
50
|
+
t.assert.equal(output, JSON.stringify(data))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('external $ref schema reused with anyOf wrapping', (t) => {
|
|
54
|
+
t.plan(2)
|
|
55
|
+
|
|
56
|
+
const contactSchema = {
|
|
57
|
+
$id: 'contact.json',
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
firstName: { type: 'string' },
|
|
61
|
+
lastName: { type: 'string' },
|
|
62
|
+
email: { type: 'string' }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Common pattern: anyOf wrapping $ref for polymorphic fields (populated object OR string ID OR null)
|
|
67
|
+
const parentSchema = {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
owner: {
|
|
71
|
+
anyOf: [{ type: ['string', 'null'] }, { $ref: 'contact.json' }]
|
|
72
|
+
},
|
|
73
|
+
assignee: {
|
|
74
|
+
anyOf: [{ type: ['string', 'null'] }, { $ref: 'contact.json' }]
|
|
75
|
+
},
|
|
76
|
+
reporter: {
|
|
77
|
+
anyOf: [{ type: ['string', 'null'] }, { $ref: 'contact.json' }]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const serializer = build(parentSchema, { schema: { 'contact.json': contactSchema } })
|
|
83
|
+
|
|
84
|
+
// Serialize with populated objects
|
|
85
|
+
const data = {
|
|
86
|
+
owner: { firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
|
|
87
|
+
assignee: { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' },
|
|
88
|
+
reporter: { firstName: 'Bob', lastName: 'Jones', email: 'bob@example.com' }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const output = serializer(data)
|
|
92
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
93
|
+
t.assert.equal(output, JSON.stringify(data))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('external $ref schema reused in array items', (t) => {
|
|
97
|
+
t.plan(2)
|
|
98
|
+
|
|
99
|
+
const contactSchema = {
|
|
100
|
+
$id: 'contact.json',
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
firstName: { type: 'string' },
|
|
104
|
+
lastName: { type: 'string' }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parentSchema = {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
contacts: {
|
|
112
|
+
type: 'array',
|
|
113
|
+
items: { $ref: 'contact.json' }
|
|
114
|
+
},
|
|
115
|
+
primary: { $ref: 'contact.json' },
|
|
116
|
+
secondary: { $ref: 'contact.json' }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const serializer = build(parentSchema, { schema: { 'contact.json': contactSchema } })
|
|
121
|
+
|
|
122
|
+
const data = {
|
|
123
|
+
contacts: [
|
|
124
|
+
{ firstName: 'Alice', lastName: 'Wonder' },
|
|
125
|
+
{ firstName: 'Bob', lastName: 'Builder' }
|
|
126
|
+
],
|
|
127
|
+
primary: { firstName: 'Charlie', lastName: 'Charm' },
|
|
128
|
+
secondary: { firstName: 'Diana', lastName: 'Prince' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const output = serializer(data)
|
|
132
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
133
|
+
t.assert.equal(output, JSON.stringify(data))
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('external array $ref schema should be reused, not inlined at every reference', (t) => {
|
|
137
|
+
t.plan(2)
|
|
138
|
+
|
|
139
|
+
const tagsSchema = {
|
|
140
|
+
$id: 'tags.json',
|
|
141
|
+
type: 'array',
|
|
142
|
+
items: { type: 'string' }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const parentSchema = {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
a: { $ref: 'tags.json' },
|
|
149
|
+
b: { $ref: 'tags.json' },
|
|
150
|
+
c: { $ref: 'tags.json' }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const serializer = build(parentSchema, { schema: { 'tags.json': tagsSchema } })
|
|
155
|
+
|
|
156
|
+
const data = {
|
|
157
|
+
a: ['x', 'y'],
|
|
158
|
+
b: ['z'],
|
|
159
|
+
c: []
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const output = serializer(data)
|
|
163
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
164
|
+
t.assert.equal(output, JSON.stringify(data))
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('inline anonymous schemas should still be inlined (not extracted)', (t) => {
|
|
168
|
+
t.plan(1)
|
|
169
|
+
|
|
170
|
+
// Inline schemas (no external $ref) should continue to work normally
|
|
171
|
+
const schema = {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
a: { type: 'object', properties: { x: { type: 'string' } } },
|
|
175
|
+
b: { type: 'object', properties: { x: { type: 'string' } } }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const serializer = build(schema)
|
|
180
|
+
const output = serializer({ a: { x: 'hello' }, b: { x: 'world' } })
|
|
181
|
+
|
|
182
|
+
t.assert.equal(output, '{"a":{"x":"hello"},"b":{"x":"world"}}')
|
|
183
|
+
})
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { test } = require('node:test')
|
|
4
|
-
const build = require('..')
|
|
5
|
-
|
|
6
|
-
test('additionalProperties: false', (t) => {
|
|
7
|
-
t.plan(1)
|
|
8
|
-
const stringify = build({
|
|
9
|
-
title: 'additionalProperties',
|
|
10
|
-
type: 'object',
|
|
11
|
-
properties: {
|
|
12
|
-
foo: {
|
|
13
|
-
type: 'string'
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
additionalProperties: false
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
const obj = { foo: 'a', bar: 'b', baz: 'c' }
|
|
20
|
-
t.assert.equal(stringify(obj), '{"foo":"a"}')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('additionalProperties: {}', (t) => {
|
|
24
|
-
t.plan(1)
|
|
25
|
-
const stringify = build({
|
|
26
|
-
title: 'additionalProperties',
|
|
27
|
-
type: 'object',
|
|
28
|
-
properties: {
|
|
29
|
-
foo: {
|
|
30
|
-
type: 'string'
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
additionalProperties: {}
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const obj = { foo: 'a', bar: 'b', baz: 'c' }
|
|
37
|
-
t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('additionalProperties: {type: string}', (t) => {
|
|
41
|
-
t.plan(1)
|
|
42
|
-
const stringify = build({
|
|
43
|
-
title: 'additionalProperties',
|
|
44
|
-
type: 'object',
|
|
45
|
-
properties: {
|
|
46
|
-
foo: {
|
|
47
|
-
type: 'string'
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
additionalProperties: {
|
|
51
|
-
type: 'string'
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const obj = { foo: 'a', bar: 'b', baz: 'c' }
|
|
56
|
-
t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
|
|
57
|
-
})
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
test('additionalProperties: false', (t) => {
|
|
7
|
+
t.plan(1)
|
|
8
|
+
const stringify = build({
|
|
9
|
+
title: 'additionalProperties',
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
foo: {
|
|
13
|
+
type: 'string'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
additionalProperties: false
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const obj = { foo: 'a', bar: 'b', baz: 'c' }
|
|
20
|
+
t.assert.equal(stringify(obj), '{"foo":"a"}')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('additionalProperties: {}', (t) => {
|
|
24
|
+
t.plan(1)
|
|
25
|
+
const stringify = build({
|
|
26
|
+
title: 'additionalProperties',
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
foo: {
|
|
30
|
+
type: 'string'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
additionalProperties: {}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const obj = { foo: 'a', bar: 'b', baz: 'c' }
|
|
37
|
+
t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('additionalProperties: {type: string}', (t) => {
|
|
41
|
+
t.plan(1)
|
|
42
|
+
const stringify = build({
|
|
43
|
+
title: 'additionalProperties',
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
foo: {
|
|
47
|
+
type: 'string'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
additionalProperties: {
|
|
51
|
+
type: 'string'
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const obj = { foo: 'a', bar: 'b', baz: 'c' }
|
|
56
|
+
t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
|
|
57
|
+
})
|
package/test/oneof.test.js
CHANGED
|
@@ -488,3 +488,23 @@ test('all array items does not match oneOf types', (t) => {
|
|
|
488
488
|
|
|
489
489
|
t.assert.throws(() => stringify({ data: [null, false, true, undefined, [], {}] }))
|
|
490
490
|
})
|
|
491
|
+
|
|
492
|
+
test('invalid oneOf schema', (t) => {
|
|
493
|
+
t.plan(1)
|
|
494
|
+
|
|
495
|
+
const schema = {
|
|
496
|
+
type: 'object',
|
|
497
|
+
properties: {
|
|
498
|
+
prop: {
|
|
499
|
+
oneOf: 'not array' // invalid, oneOf must be array
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
build(schema)
|
|
506
|
+
t.assert.fail('Should throw')
|
|
507
|
+
} catch (err) {
|
|
508
|
+
t.assert.ok(err.message.includes('schema is invalid'))
|
|
509
|
+
}
|
|
510
|
+
})
|
package/test/ref.test.js
CHANGED
|
@@ -2044,3 +2044,34 @@ test('ref internal - throw if schema has definition twice with different shape',
|
|
|
2044
2044
|
t.assert.equal(err.message, 'There is already another anchor "#uri" in schema "test".')
|
|
2045
2045
|
}
|
|
2046
2046
|
})
|
|
2047
|
+
|
|
2048
|
+
test('ref nested', (t) => {
|
|
2049
|
+
t.plan(2)
|
|
2050
|
+
|
|
2051
|
+
const schema = {
|
|
2052
|
+
definitions: {
|
|
2053
|
+
def1: {
|
|
2054
|
+
$ref: '#/definitions/def2'
|
|
2055
|
+
},
|
|
2056
|
+
def2: {
|
|
2057
|
+
type: 'string'
|
|
2058
|
+
}
|
|
2059
|
+
},
|
|
2060
|
+
type: 'object',
|
|
2061
|
+
properties: {
|
|
2062
|
+
str: {
|
|
2063
|
+
$ref: '#/definitions/def1'
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
const object = {
|
|
2069
|
+
str: 'test'
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
const stringify = build(schema)
|
|
2073
|
+
const output = stringify(object)
|
|
2074
|
+
|
|
2075
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
2076
|
+
t.assert.equal(output, '{"str":"test"}')
|
|
2077
|
+
})
|
package/test/typebox.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Test using this disabled, see https://github.com/fastify/fast-json-stringify/pull/683
|
|
2
2
|
import Ajv from 'ajv'
|
|
3
3
|
import build, { restore, Schema, validLargeArrayMechanisms } from '..'
|
|
4
|
-
import {
|
|
4
|
+
import { expect } from 'tstyche'
|
|
5
5
|
|
|
6
6
|
// Number schemas
|
|
7
7
|
build({
|
|
@@ -26,9 +26,10 @@ build({
|
|
|
26
26
|
build({
|
|
27
27
|
type: 'number'
|
|
28
28
|
}, { rounding: 'trunc' })
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
expect(build).type.not.toBeCallableWith({
|
|
30
31
|
type: 'number'
|
|
31
|
-
}, { rounding: 'invalid' })
|
|
32
|
+
}, { rounding: 'invalid' })
|
|
32
33
|
|
|
33
34
|
// String schema
|
|
34
35
|
build({
|
|
@@ -153,7 +154,6 @@ build({
|
|
|
153
154
|
})({ something: 'a string', somethingElse: 42 })
|
|
154
155
|
|
|
155
156
|
// String schema with format
|
|
156
|
-
|
|
157
157
|
build({
|
|
158
158
|
type: 'string',
|
|
159
159
|
format: 'date-time'
|
|
@@ -181,7 +181,6 @@ str = build({
|
|
|
181
181
|
type: 'number'
|
|
182
182
|
}, { mode: 'standalone' })
|
|
183
183
|
*/
|
|
184
|
-
|
|
185
184
|
const debugCompiled = build({
|
|
186
185
|
title: 'default string',
|
|
187
186
|
type: 'object',
|
|
@@ -191,11 +190,12 @@ const debugCompiled = build({
|
|
|
191
190
|
}
|
|
192
191
|
}
|
|
193
192
|
}, { mode: 'debug' })
|
|
194
|
-
expectType<ReturnType<typeof build>>(build.restore(debugCompiled))
|
|
195
|
-
expectType<ReturnType<typeof build>>(restore(debugCompiled))
|
|
196
193
|
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
expect(build.restore(debugCompiled)).type.toBe(build({} as Schema))
|
|
195
|
+
expect(restore(debugCompiled)).type.toBe(build({} as Schema))
|
|
196
|
+
|
|
197
|
+
expect(build.validLargeArrayMechanisms).type.toBe<string[]>()
|
|
198
|
+
expect(validLargeArrayMechanisms).type.toBe<string[]>()
|
|
199
199
|
|
|
200
200
|
/**
|
|
201
201
|
* Schema inference
|
|
@@ -211,10 +211,11 @@ const stringify3 = build({
|
|
|
211
211
|
type: 'object',
|
|
212
212
|
properties: { a: { type: 'string' } },
|
|
213
213
|
})
|
|
214
|
+
|
|
214
215
|
stringify3<InferenceSchema>({ id: '123' })
|
|
215
216
|
stringify3<InferenceSchema>({ a: 123, id: '123' })
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
expect(stringify3<InferenceSchema>).type.not.toBeCallableWith({ anotherOne: 'bar' })
|
|
218
|
+
expect(stringify3<Schema>).type.not.toBeCallableWith({ a: 'bar' })
|
|
218
219
|
|
|
219
220
|
// Without inference
|
|
220
221
|
const stringify4 = build({
|
|
@@ -231,29 +232,28 @@ const stringify5 = build({
|
|
|
231
232
|
type: 'string',
|
|
232
233
|
})
|
|
233
234
|
stringify5('foo')
|
|
234
|
-
|
|
235
|
+
expect(stringify5).type.not.toBeCallableWith({ id: '123' })
|
|
235
236
|
|
|
236
237
|
// Without inference - null type
|
|
237
238
|
const stringify6 = build({
|
|
238
239
|
type: 'null',
|
|
239
240
|
})
|
|
240
241
|
stringify6(null)
|
|
241
|
-
|
|
242
|
+
expect(stringify6).type.not.toBeCallableWith('a string')
|
|
242
243
|
|
|
243
244
|
// Without inference - boolean type
|
|
244
245
|
const stringify7 = build({
|
|
245
246
|
type: 'boolean',
|
|
246
247
|
})
|
|
247
248
|
stringify7(true)
|
|
248
|
-
|
|
249
|
+
expect(stringify7).type.not.toBeCallableWith('a string')
|
|
249
250
|
|
|
250
251
|
// largeArrayMechanism
|
|
251
|
-
|
|
252
252
|
build({}, { largeArrayMechanism: 'json-stringify' })
|
|
253
253
|
build({}, { largeArrayMechanism: 'default' })
|
|
254
|
-
|
|
254
|
+
expect(build).type.not.toBeCallableWith({} as Schema, { largeArrayMechanism: 'invalid' })
|
|
255
255
|
|
|
256
256
|
build({}, { largeArraySize: 2000 })
|
|
257
257
|
build({}, { largeArraySize: '2e4' })
|
|
258
258
|
build({}, { largeArraySize: 2n })
|
|
259
|
-
|
|
259
|
+
expect(build).type.not.toBeCallableWith({} as Schema, { largeArraySize: ['asdf'] })
|
package/.github/stale.yml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Number of days of inactivity before an issue becomes stale
|
|
2
|
-
daysUntilStale: 15
|
|
3
|
-
# Number of days of inactivity before a stale issue is closed
|
|
4
|
-
daysUntilClose: 7
|
|
5
|
-
# Issues with these labels will never be considered stale
|
|
6
|
-
exemptLabels:
|
|
7
|
-
- "discussion"
|
|
8
|
-
- "feature request"
|
|
9
|
-
- "bug"
|
|
10
|
-
- "help wanted"
|
|
11
|
-
- "plugin suggestion"
|
|
12
|
-
- "good first issue"
|
|
13
|
-
# Label to use when marking an issue as stale
|
|
14
|
-
staleLabel: stale
|
|
15
|
-
# Comment to post when marking an issue as stale. Set to `false` to disable
|
|
16
|
-
markComment: >
|
|
17
|
-
This issue has been automatically marked as stale because it has not had
|
|
18
|
-
recent activity. It will be closed if no further activity occurs. Thank you
|
|
19
|
-
for your contributions.
|
|
20
|
-
# Comment to post when closing a stale issue. Set to `false` to disable
|
|
21
|
-
closeComment: false
|