fast-json-stringify 6.3.0 → 7.0.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.
@@ -27,7 +27,7 @@ jobs:
27
27
  permissions:
28
28
  contents: write
29
29
  pull-requests: write
30
- uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
30
+ uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6
31
31
  with:
32
32
  license-check: true
33
33
  lint: true
@@ -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
@@ -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('FJS creation', function () {
176
+ suite.add('fast-json-stringify: creation', function () {
179
177
  FJS(schema)
180
178
  })
181
- suite.add('CJS creation', function () {
179
+ suite.add('compile-json-stringify: creation', function () {
182
180
  CJS(schemaCJS)
183
181
  })
184
- suite.add('AJV Serialize creation', function () {
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 default', function () {
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 array json-stringify', function () {
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 Serialize array', function () {
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 default', function () {
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 json-stringify', function () {
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 Serialize large array', function () {
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 Serialize long string', function () {
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 Serialize short string', function () {
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 Serialize obj', function () {
289
+ suite.add('AJV: obj', function () {
292
290
  ajvSerialize(obj)
293
291
  })
294
292
 
295
- suite.add('JSON stringify date', function () {
293
+ suite.add('JSON.stringify: date', function () {
296
294
  JSON.stringify(date)
297
295
  })
298
296
 
299
- suite.add('fast-json-stringify date format', function () {
297
+ suite.add('fast-json-stringify: date', function () {
300
298
  stringifyDate(date)
301
299
  })
302
300
 
303
- suite.add('json-accelerate date format', function () {
301
+ suite.add('json-accelerate: date', function () {
304
302
  accelDate(date)
305
303
  })
306
304
 
307
- suite.add('compile-json-stringify date format', function () {
305
+ suite.add('compile-json-stringify: date', function () {
308
306
  CJSStringifyDate(date)
309
307
  })
310
308
 
311
309
  suite.run().then(() => {
312
- for (const task of suite.tasks) {
313
- const hz = task.result.throughput.mean // ops/sec
314
- const rme = task.result.latency.rme // relative margin of error (%)
315
- const samples = task.result.latency.df + 1 // degrees of freedom + 1 = sample count
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
- const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 })
318
- const formattedRme = rme.toFixed(2)
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 output = `${task.name} x ${formattedHz} ops/sec ±${formattedRme}% (${samples} runs sampled)`
321
- console.log(output)
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}`))
package/index.js CHANGED
@@ -222,10 +222,6 @@ function build (schema, options) {
222
222
  }
223
223
  }
224
224
 
225
- if (options.debugMode) {
226
- options.mode = 'debug'
227
- }
228
-
229
225
  if (options.mode === 'debug') {
230
226
  return {
231
227
  validator,
@@ -568,7 +564,7 @@ function buildObject (context, location, input) {
568
564
  const jsonPointer = location.jsonPointer || ''
569
565
  const fullPath = `${schemaId}#${jsonPointer}`
570
566
 
571
- if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
567
+ if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema) || schemaId !== '') {
572
568
  const functionName = generateFuncName(context)
573
569
  context.functionsNamesBySchema.set(schema, functionName)
574
570
 
@@ -627,7 +623,7 @@ function buildArray (context, location, input) {
627
623
  const jsonPointer = location.jsonPointer || ''
628
624
  const fullPath = `${schemaId}#${jsonPointer}`
629
625
 
630
- if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
626
+ if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema) || schemaId !== '') {
631
627
  const functionName = generateFuncName(context)
632
628
  context.functionsNamesBySchema.set(schema, functionName)
633
629
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "6.3.0",
3
+ "version": "7.0.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": "tsd",
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,19 @@
62
62
  }
63
63
  ],
64
64
  "devDependencies": {
65
- "@sinclair/typebox": "^0.34.48",
66
- "c8": "^10.1.3",
65
+ "c8": "^11.0.0",
67
66
  "cli-select": "^1.1.2",
68
67
  "compile-json-stringify": "^0.1.2",
69
68
  "eslint": "^9.39.2",
70
69
  "fast-json-stringify": ".",
71
70
  "is-my-json-valid": "^2.20.6",
72
71
  "json-accelerator": "^0.1.7",
73
- "neostandard": "^0.12.2",
72
+ "neostandard": "^0.13.0",
74
73
  "simple-git": "^3.30.0",
75
74
  "tinybench": "^6.0.0",
76
- "tsd": "^0.33.0",
77
- "typescript": "^5.9.3",
75
+ "tstyche": "^7.1.0",
76
+ "typebox": "^1.0.81",
77
+ "typescript": "~6.0.2",
78
78
  "webpack": "^5.104.1"
79
79
  },
80
80
  "dependencies": {
@@ -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
+ })
@@ -386,6 +386,28 @@ test('array items is a schema and additionalItems is false', (t) => {
386
386
  t.assert.equal(validate({ foo: ['foo', 'bar'] }), true)
387
387
  })
388
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
+
389
411
  // https://github.com/fastify/fast-json-stringify/issues/279
390
412
  test('object array with anyOf and symbol', (t) => {
391
413
  t.plan(1)
@@ -22,7 +22,7 @@ function build (opts) {
22
22
 
23
23
  test('activate debug mode', t => {
24
24
  t.plan(5)
25
- const debugMode = build({ debugMode: true })
25
+ const debugMode = build({ mode: 'debug' })
26
26
 
27
27
  t.assert.ok(typeof debugMode === 'object')
28
28
  t.assert.ok(debugMode.ajv instanceof Ajv)
@@ -31,21 +31,9 @@ test('activate debug mode', t => {
31
31
  t.assert.ok(typeof debugMode.code === 'string')
32
32
  })
33
33
 
34
- test('activate debug mode truthy', t => {
35
- t.plan(5)
36
-
37
- const debugMode = build({ debugMode: 'yes' })
38
-
39
- t.assert.ok(typeof debugMode === 'object')
40
- t.assert.ok(typeof debugMode.code === 'string')
41
- t.assert.ok(debugMode.ajv instanceof Ajv)
42
- t.assert.ok(debugMode.validator instanceof Validator)
43
- t.assert.ok(debugMode.serializer instanceof Serializer)
44
- })
45
-
46
34
  test('to string auto-consistent', t => {
47
35
  t.plan(6)
48
- const debugMode = build({ debugMode: 1 })
36
+ const debugMode = build({ mode: 'debug' })
49
37
 
50
38
  t.assert.ok(typeof debugMode === 'object')
51
39
  t.assert.ok(typeof debugMode.code === 'string')
@@ -73,7 +61,7 @@ test('to string auto-consistent with ajv', t => {
73
61
  }]
74
62
  }
75
63
  }
76
- }, { debugMode: 1 })
64
+ }, { mode: 'debug' })
77
65
 
78
66
  t.assert.ok(typeof debugMode === 'object')
79
67
  t.assert.ok(typeof debugMode.code === 'string')
@@ -102,7 +90,7 @@ test('to string auto-consistent with ajv-formats', t => {
102
90
  }]
103
91
  }
104
92
  }
105
- }, { debugMode: 1 })
93
+ }, { mode: 'debug' })
106
94
 
107
95
  t.assert.ok(typeof debugMode === 'object')
108
96
 
@@ -115,7 +103,28 @@ test('to string auto-consistent with ajv-formats', t => {
115
103
  test('debug should restore the same serializer instance', t => {
116
104
  t.plan(1)
117
105
 
118
- const debugMode = fjs({ type: 'integer' }, { debugMode: 1, rounding: 'ceil' })
106
+ const debugMode = fjs({ type: 'integer' }, { mode: 'debug', rounding: 'ceil' })
119
107
  const compiled = fjs.restore(debugMode)
120
108
  t.assert.equal(compiled(3.95), 4)
121
109
  })
110
+
111
+ test('Serializer restoreFromState', t => {
112
+ t.plan(1)
113
+
114
+ const Serializer = require('../lib/serializer')
115
+ const serializer = new Serializer()
116
+ const state = serializer.getState()
117
+ const restored = Serializer.restoreFromState(state)
118
+ t.assert.ok(restored instanceof Serializer)
119
+ })
120
+
121
+ test('Validator restoreFromState', t => {
122
+ t.plan(1)
123
+
124
+ const Validator = require('../lib/validator')
125
+ const validator = new Validator()
126
+ validator.addSchema({ type: 'string' }, 'test')
127
+ const state = validator.getState()
128
+ const restored = Validator.restoreFromState(state)
129
+ t.assert.ok(restored instanceof Validator)
130
+ })
@@ -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
+ })
@@ -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
+ })
@@ -4,7 +4,7 @@ const { test } = require('node:test')
4
4
  const build = require('..')
5
5
 
6
6
  test('nested object in pattern properties for typebox', (t) => {
7
- const { Type } = require('@sinclair/typebox')
7
+ const { Type } = require('typebox')
8
8
 
9
9
  t.plan(1)
10
10
 
package/types/index.d.ts CHANGED
@@ -162,16 +162,10 @@ declare namespace build {
162
162
  * @default 'trunc'
163
163
  */
164
164
  rounding?: 'ceil' | 'floor' | 'round' | 'trunc'
165
- /**
166
- * @deprecated
167
- * Enable debug mode. Please use `mode: "debug"` instead
168
- */
169
- debugMode?: boolean
170
165
  /**
171
166
  * Running mode of fast-json-stringify
172
167
  */
173
168
  mode?: 'debug' | 'standalone'
174
-
175
169
  /**
176
170
  * Large arrays are defined as arrays containing, by default, `20000`
177
171
  * elements or more. That value can be adjusted via the option parameter
@@ -180,7 +174,6 @@ declare namespace build {
180
174
  * @default 20000
181
175
  */
182
176
  largeArraySize?: number | string | BigInt
183
-
184
177
  /**
185
178
  * Specify the function on how large Arrays should be stringified.
186
179
  *
@@ -200,10 +193,6 @@ interface DebugOption extends build.Options {
200
193
  mode: 'debug'
201
194
  }
202
195
 
203
- interface DeprecateDebugOption extends build.Options {
204
- debugMode: true
205
- }
206
-
207
196
  interface StandaloneOption extends build.Options {
208
197
  mode: 'standalone'
209
198
  }
@@ -217,7 +206,6 @@ type IntegerCoercible = number | BigInt
217
206
  * @param options The options to use (optional)
218
207
  */
219
208
  declare function build (schema: build.AnySchema, options: DebugOption): { code: string, ajv: Ajv }
220
- declare function build (schema: build.AnySchema, options: DeprecateDebugOption): { code: string, ajv: Ajv }
221
209
  declare function build (schema: build.AnySchema, options: StandaloneOption): string
222
210
  declare function build (schema: build.AnySchema, options?: build.Options): <TDoc = any>(doc: TDoc) => any
223
211
  declare function build (schema: build.StringSchema, options?: build.Options): <TDoc extends StringCoercible = StringCoercible>(doc: TDoc) => string
@@ -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 { expectError, expectType } from 'tsd'
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
- expectError(build({
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
- expectType<string[]>(build.validLargeArrayMechanisms)
198
- expectType<string[]>(validLargeArrayMechanisms)
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
- expectError(stringify3<InferenceSchema>({ anotherOne: 'bar' }))
217
- expectError(stringify3<Schema>({ a: 'bar' }))
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
- expectError(stringify5({ id: '123' }))
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
- expectError(stringify6('a string'))
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
- expectError(stringify7('a string'))
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
- expectError(build({} as Schema, { largeArrayMechanism: 'invalid' }))
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
- expectError(build({} as Schema, { largeArraySize: ['asdf'] }))
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