fast-json-stringify 7.0.1 → 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.
Files changed (40) hide show
  1. package/.github/dependabot.yml +1 -0
  2. package/.github/workflows/ci.yml +1 -1
  3. package/.github/workflows/lock-threads.yml +1 -1
  4. package/.github/workflows/release.yml +24 -0
  5. package/README.md +20 -3
  6. package/benchmark/bench-cmp-lib.js +63 -38
  7. package/benchmark/bench-thread.js +8 -2
  8. package/benchmark/bench.js +86 -0
  9. package/benchmark/fixtures/api-responses.js +266 -0
  10. package/index.js +366 -98
  11. package/lib/location.js +3 -1
  12. package/lib/serializer.js +41 -37
  13. package/lib/standalone.js +22 -3
  14. package/lib/validator.js +22 -3
  15. package/package.json +4 -5
  16. package/test/additionalProperties.test.js +40 -0
  17. package/test/anyof.test.js +42 -0
  18. package/test/array.test.js +26 -1
  19. package/test/basic.test.js +1 -1
  20. package/test/code-generation-fallbacks.test.js +158 -0
  21. package/test/compile-validators.test.js +110 -0
  22. package/test/const.test.js +7 -6
  23. package/test/date.test.js +28 -1
  24. package/test/debug-mode.test.js +8 -0
  25. package/test/deopt.test.js +251 -0
  26. package/test/if-then-else.test.js +145 -0
  27. package/test/inferType.test.js +1 -1
  28. package/test/integer.test.js +1 -1
  29. package/test/oneof.test.js +242 -0
  30. package/test/optional-property-prefix.test.js +119 -0
  31. package/test/property-names.test.js +41 -0
  32. package/test/regex.test.js +1 -1
  33. package/test/schema-depth.test.js +227 -0
  34. package/test/standalone-mode.test.js +109 -2
  35. package/test/string.test.js +8 -0
  36. package/test/surrogate.test.js +1 -1
  37. package/test/typesArray.test.js +59 -0
  38. package/test/utils/validator.js +20 -0
  39. package/types/index.d.ts +20 -0
  40. package/types/index.tst.ts +13 -0
@@ -34,6 +34,7 @@ updates:
34
34
  - dependency-name: "eslint"
35
35
  - dependency-name: "neostandard"
36
36
  - dependency-name: "@stylistic/*"
37
+ - dependency-name: "typescript"
37
38
  open-pull-requests-limit: 10
38
39
  groups:
39
40
  # Production dependencies with breaking changes
@@ -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@v6
30
+ uses: fastify/workflows/.github/workflows/plugins-ci.yml@ef591e2186785d5ab36b9fe6a79c7ce2f1d94e57 # v7.0.0
31
31
  with:
32
32
  license-check: true
33
33
  lint: true
@@ -16,4 +16,4 @@ jobs:
16
16
  permissions:
17
17
  issues: write
18
18
  pull-requests: write
19
- uses: fastify/workflows/.github/workflows/lock-threads.yml@v6
19
+ uses: fastify/workflows/.github/workflows/lock-threads.yml@ef591e2186785d5ab36b9fe6a79c7ce2f1d94e57 # v7.0.0
@@ -0,0 +1,24 @@
1
+ name: Release the package
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ semver:
7
+ description: 'Release bump type'
8
+ required: true
9
+ type: choice
10
+ options:
11
+ - patch
12
+ - minor
13
+ - major
14
+
15
+ permissions: {}
16
+
17
+ jobs:
18
+ release:
19
+ permissions:
20
+ id-token: write # required for npm provenance via OIDC
21
+ contents: write # required to push the release commit and the tag
22
+ uses: fastify/workflows/.github/workflows/reusable-release.yml@b1a11bce713bc857e52b2231fec3c89071ad3135
23
+ with:
24
+ semver: ${{ inputs.semver }}
package/README.md CHANGED
@@ -121,15 +121,24 @@ const fastJson = require('fast-json-stringify')
121
121
  const stringify = fastJson(mySchema, {
122
122
  schema: { ... },
123
123
  ajv: { ... },
124
- rounding: 'ceil'
124
+ rounding: 'ceil',
125
+ maxDepth: 100
125
126
  })
126
127
  ```
127
128
 
128
129
  - `schema`: external schemas references by $ref property. [More details](#ref)
129
130
  - `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof)
130
131
  - `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
132
+ - `maxDepth`: maximum number of nested schema levels allowed during compilation.
133
+ Defaults to `100` and may be set from `0` to `100`. Schemas that exceed this limit
134
+ are rejected before compilation.
135
+ - `inlineValidators`: when using standalone mode, embed Ajv-generated validator functions in the output instead of compiling schemas at runtime. [More details](#standalone)
131
136
  - `largeArrayMechanism`: set the mechanism that should be used to handle large
132
137
  (by default `20000` or more items) arrays. [More details](#largearrays)
138
+ - `compileValidators`: when `true`, the `ajv` validators used by `anyOf`, `oneOf` and
139
+ `if/then/else` are compiled during `build()` instead of lazily, on the first serialization
140
+ that reaches them. This makes `build()` slower but removes a potentially large one-off cost
141
+ from the first `stringify()` call. Defaults to `false`.
133
142
 
134
143
 
135
144
  <a name="api"></a>
@@ -168,7 +177,7 @@ And nested ones, too.
168
177
  | `date` | `2020-04-03` |
169
178
  | `time` | `09:11:08` |
170
179
 
171
- **Note**: In the case of a string formatted Date and not Date Object, there will be no manipulation on it. It should be properly formatted.
180
+ **Note**: In the case of a string formatted Date and not Date Object, the value is not reformatted nor validated: it should already be properly formatted. It is only escaped, so that the resulting document is always valid JSON.
172
181
 
173
182
  Example with a Date object:
174
183
 
@@ -724,13 +733,21 @@ const code = fastJson({
724
733
  type: 'string'
725
734
  }
726
735
  }
727
- }, { mode: 'standalone' })
736
+ }, { mode: 'standalone', inlineValidators: true })
728
737
 
729
738
  fs.writeFileSync('stringify.js', code)
730
739
  const stringify = require('stringify.js')
731
740
  console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
732
741
  ```
733
742
 
743
+ Set `inlineValidators` to `true` to include the Ajv-generated functions used by
744
+ `anyOf`, `oneOf`, and `if/then/else` in the same output file. This avoids
745
+ rebuilding an Ajv instance and compiling their schemas at runtime. The
746
+ generated module still requires `fast-json-stringify` for its serializer and
747
+ may require Ajv runtime helpers used by the generated validation code. Custom
748
+ Ajv formats used by these schemas must support Ajv's standalone code
749
+ generation.
750
+
734
751
  <a name="acknowledgments"></a>
735
752
  ## Acknowledgments
736
753
 
@@ -1,6 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ const assert = require('node:assert/strict')
3
4
  const { Bench } = require('tinybench')
5
+ const apiResponses = require('./fixtures/api-responses')
4
6
  const suite = new Bench({
5
7
  name: 'Library Comparison Benchmarks',
6
8
  setup: (_task, mode) => {
@@ -173,139 +175,162 @@ for (let i = 0; i < MULTI_ARRAY_LENGTH; i++) {
173
175
  multiArray[i] = obj
174
176
  }
175
177
 
176
- suite.add('fast-json-stringify: creation', function () {
177
- FJS(schema)
178
+ // Some serializers return unflattened strings. Fastify calls Buffer.byteLength()
179
+ // for Content-Length, which triggers flattening; include that cost in every case.
180
+ suite.add('fast-json-stringify: creation and first stringify', function () {
181
+ Buffer.byteLength(FJS(schema)(obj))
178
182
  })
179
- suite.add('compile-json-stringify: creation', function () {
180
- CJS(schemaCJS)
183
+ suite.add('compile-json-stringify: creation and first stringify', function () {
184
+ Buffer.byteLength(CJS(schemaCJS)(obj))
181
185
  })
182
- suite.add('AJV: creation', function () {
183
- ajv.compileSerializer(schemaAJVJTD)
186
+ suite.add('AJV: creation and first stringify', function () {
187
+ Buffer.byteLength(ajv.compileSerializer(schemaAJVJTD)(obj))
184
188
  })
185
- suite.add('json-accelerator: creation', function () {
186
- createAccelerator(schema)
189
+ suite.add('json-accelerator: creation and first stringify', function () {
190
+ Buffer.byteLength(createAccelerator(schema)(obj))
187
191
  })
188
192
 
189
193
  suite.add('JSON.stringify: array', function () {
190
- JSON.stringify(multiArray)
194
+ Buffer.byteLength(JSON.stringify(multiArray))
191
195
  })
192
196
 
193
197
  suite.add('fast-json-stringify [default]: array', function () {
194
- stringifyArrayDefault(multiArray)
198
+ Buffer.byteLength(stringifyArrayDefault(multiArray))
195
199
  })
196
200
 
197
201
  suite.add('json-accelerator: array', function () {
198
- accelArray(multiArray)
202
+ Buffer.byteLength(accelArray(multiArray))
199
203
  })
200
204
 
201
205
  suite.add('fast-json-stringify [json-stringify]: array', function () {
202
- stringifyArrayJSONStringify(multiArray)
206
+ Buffer.byteLength(stringifyArrayJSONStringify(multiArray))
203
207
  })
204
208
 
205
209
  suite.add('compile-json-stringify: array', function () {
206
- CJSStringifyArray(multiArray)
210
+ Buffer.byteLength(CJSStringifyArray(multiArray))
207
211
  })
208
212
 
209
213
  suite.add('AJV: array', function () {
210
- ajvSerializeArray(multiArray)
214
+ Buffer.byteLength(ajvSerializeArray(multiArray))
211
215
  })
212
216
 
213
217
  suite.add('JSON.stringify: large array', function () {
214
- JSON.stringify(largeArray)
218
+ Buffer.byteLength(JSON.stringify(largeArray))
215
219
  })
216
220
 
217
221
  suite.add('fast-json-stringify [default]: large array', function () {
218
- stringifyArrayDefault(largeArray)
222
+ Buffer.byteLength(stringifyArrayDefault(largeArray))
219
223
  })
220
224
 
221
225
  suite.add('fast-json-stringify [json-stringify]: large array', function () {
222
- stringifyArrayJSONStringify(largeArray)
226
+ Buffer.byteLength(stringifyArrayJSONStringify(largeArray))
223
227
  })
224
228
 
225
229
  suite.add('compile-json-stringify: large array', function () {
226
- CJSStringifyArray(largeArray)
230
+ Buffer.byteLength(CJSStringifyArray(largeArray))
227
231
  })
228
232
 
229
233
  suite.add('AJV: large array', function () {
230
- ajvSerializeArray(largeArray)
234
+ Buffer.byteLength(ajvSerializeArray(largeArray))
231
235
  })
232
236
 
233
237
  suite.add('JSON.stringify: long string', function () {
234
- JSON.stringify(str)
238
+ Buffer.byteLength(JSON.stringify(str))
235
239
  })
236
240
 
237
241
  suite.add('fast-json-stringify: long string', function () {
238
- stringifyString(str)
242
+ Buffer.byteLength(stringifyString(str))
239
243
  })
240
244
 
241
245
  suite.add('json-accelerator: long string', function () {
242
- stringifyString(str)
246
+ Buffer.byteLength(stringifyString(str))
243
247
  })
244
248
 
245
249
  suite.add('compile-json-stringify: long string', function () {
246
- CJSStringifyString(str)
250
+ Buffer.byteLength(CJSStringifyString(str))
247
251
  })
248
252
 
249
253
  suite.add('AJV: long string', function () {
250
- ajvSerializeString(str)
254
+ Buffer.byteLength(ajvSerializeString(str))
251
255
  })
252
256
 
253
257
  suite.add('JSON.stringify: short string', function () {
254
- JSON.stringify('hello world')
258
+ Buffer.byteLength(JSON.stringify('hello world'))
255
259
  })
256
260
 
257
261
  suite.add('fast-json-stringify: short string', function () {
258
- stringifyString('hello world')
262
+ Buffer.byteLength(stringifyString('hello world'))
259
263
  })
260
264
 
261
265
  suite.add('json-accelerator: short string', function () {
262
- accelString('hello world')
266
+ Buffer.byteLength(accelString('hello world'))
263
267
  })
264
268
 
265
269
  suite.add('compile-json-stringify: short string', function () {
266
- CJSStringifyString('hello world')
270
+ Buffer.byteLength(CJSStringifyString('hello world'))
267
271
  })
268
272
 
269
273
  suite.add('AJV: short string', function () {
270
- ajvSerializeString('hello world')
274
+ Buffer.byteLength(ajvSerializeString('hello world'))
271
275
  })
272
276
 
273
277
  suite.add('JSON.stringify: obj', function () {
274
- JSON.stringify(obj)
278
+ Buffer.byteLength(JSON.stringify(obj))
275
279
  })
276
280
 
277
281
  suite.add('fast-json-stringify: obj', function () {
278
- stringify(obj)
282
+ Buffer.byteLength(stringify(obj))
279
283
  })
280
284
 
281
285
  suite.add('json-accelerator: obj', function () {
282
- accelStringify(obj)
286
+ Buffer.byteLength(accelStringify(obj))
283
287
  })
284
288
 
285
289
  suite.add('compile-json-stringify: obj', function () {
286
- CJSStringify(obj)
290
+ Buffer.byteLength(CJSStringify(obj))
287
291
  })
288
292
 
289
293
  suite.add('AJV: obj', function () {
290
- ajvSerialize(obj)
294
+ Buffer.byteLength(ajvSerialize(obj))
291
295
  })
292
296
 
293
297
  suite.add('JSON.stringify: date', function () {
294
- JSON.stringify(date)
298
+ Buffer.byteLength(JSON.stringify(date))
295
299
  })
296
300
 
297
301
  suite.add('fast-json-stringify: date', function () {
298
- stringifyDate(date)
302
+ Buffer.byteLength(stringifyDate(date))
299
303
  })
300
304
 
301
305
  suite.add('json-accelerate: date', function () {
302
- accelDate(date)
306
+ Buffer.byteLength(accelDate(date))
303
307
  })
304
308
 
305
309
  suite.add('compile-json-stringify: date', function () {
306
- CJSStringifyDate(date)
310
+ Buffer.byteLength(CJSStringifyDate(date))
307
311
  })
308
312
 
313
+ for (const { name, schema: responseSchema, input } of apiResponses) {
314
+ const stringifyResponse = FJS(responseSchema)
315
+ // Check outside the timed loop that the schema preserves the complete response.
316
+ assert.deepStrictEqual(JSON.parse(stringifyResponse(input)), JSON.parse(JSON.stringify(input)))
317
+
318
+ suite.add(`JSON.stringify: ${name}`, function () {
319
+ Buffer.byteLength(JSON.stringify(input))
320
+ })
321
+ suite.add(`fast-json-stringify: ${name}`, function () {
322
+ Buffer.byteLength(stringifyResponse(input))
323
+ })
324
+ }
325
+
326
+ if (process.argv.includes('--fjs-vs-json')) {
327
+ for (const task of suite.tasks) {
328
+ if (!task.name.startsWith('fast-json-stringify') && !task.name.startsWith('JSON.stringify:')) {
329
+ suite.remove(task.name)
330
+ }
331
+ }
332
+ }
333
+
309
334
  suite.run().then(() => {
310
335
  const results = suite.tasks.map(task => ({
311
336
  name: task.name,
@@ -15,10 +15,16 @@ const bench = new Bench({
15
15
  })
16
16
 
17
17
  const FJS = require('..')
18
- const stringify = FJS(benchmark.schema)
18
+ const stringify = benchmark.compile ? null : FJS(benchmark.schema, benchmark.options)
19
19
 
20
20
  bench.add(benchmark.name, () => {
21
- stringify(benchmark.input)
21
+ // Some serializers return unflattened strings. Fastify calls Buffer.byteLength()
22
+ // for Content-Length, which triggers flattening; include that cost here too.
23
+ if (benchmark.compile) {
24
+ Buffer.byteLength(FJS(benchmark.schema, benchmark.options)(benchmark.input))
25
+ } else {
26
+ Buffer.byteLength(stringify(benchmark.input))
27
+ }
22
28
  }).run().then(() => {
23
29
  const task = bench.tasks[0]
24
30
  const hz = task.result.throughput.mean // ops/sec
@@ -300,6 +300,92 @@ const benchmarks = [
300
300
  },
301
301
  input: { firstName: 'Max', lastName: 'Power', age: 22 }
302
302
  },
303
+ {
304
+ name: 'oneOf external refs compile and first stringify',
305
+ compile: true,
306
+ schema: {
307
+ title: 'External refs oneOf schema',
308
+ type: 'object',
309
+ properties: {
310
+ content: {
311
+ oneOf: [
312
+ { $ref: 'BenchmarkFoo#' },
313
+ { $ref: 'BenchmarkBar#' },
314
+ { $ref: 'BenchmarkBaz#' }
315
+ ]
316
+ }
317
+ }
318
+ },
319
+ options: {
320
+ schema: {
321
+ BenchmarkFoo: {
322
+ type: 'object',
323
+ properties: {
324
+ kind: { const: 'foo' },
325
+ title: { type: 'string' },
326
+ count: { type: 'integer' },
327
+ flags: {
328
+ type: 'array',
329
+ items: { type: 'string' }
330
+ }
331
+ },
332
+ required: ['kind', 'title', 'count', 'flags'],
333
+ additionalProperties: false
334
+ },
335
+ BenchmarkBar: {
336
+ type: 'object',
337
+ properties: {
338
+ kind: { const: 'bar' },
339
+ name: { type: 'string' },
340
+ value: { type: 'number' },
341
+ meta: {
342
+ type: 'object',
343
+ properties: {
344
+ active: { type: 'boolean' },
345
+ tags: {
346
+ type: 'array',
347
+ items: { type: 'string' }
348
+ }
349
+ },
350
+ required: ['active', 'tags'],
351
+ additionalProperties: false
352
+ }
353
+ },
354
+ required: ['kind', 'name', 'value', 'meta'],
355
+ additionalProperties: false
356
+ },
357
+ BenchmarkBaz: {
358
+ type: 'object',
359
+ properties: {
360
+ kind: { const: 'baz' },
361
+ id: { type: 'string' },
362
+ nested: {
363
+ type: 'object',
364
+ properties: {
365
+ score: { type: 'number' },
366
+ note: { type: 'string' }
367
+ },
368
+ required: ['score', 'note'],
369
+ additionalProperties: false
370
+ }
371
+ },
372
+ required: ['kind', 'id', 'nested'],
373
+ additionalProperties: false
374
+ }
375
+ }
376
+ },
377
+ input: {
378
+ content: {
379
+ kind: 'bar',
380
+ name: 'benchmark',
381
+ value: 42.5,
382
+ meta: {
383
+ active: true,
384
+ tags: ['one', 'two', 'three']
385
+ }
386
+ }
387
+ }
388
+ },
303
389
  {
304
390
  name: 'object with const string property',
305
391
  schema: {