fast-json-stringify 7.1.2 → 7.1.3
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/README.md +1 -1
- package/index.js +5 -2
- package/lib/serializer.js +4 -1
- package/lib/validator.js +2 -2
- package/package.json +1 -1
- package/test/primitive-array-prototype.test.js +51 -0
- package/test/standalone-mode.test.js +32 -0
- package/test/strict-schema.test.js +55 -0
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ const stringify = fastJson(mySchema, {
|
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
- `schema`: external schemas references by $ref property. [More details](#ref)
|
|
130
|
-
- `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof)
|
|
130
|
+
- `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof) Schema strictness defaults to `false`; pass `ajv: { strictSchema: true }` to reject unknown schema keywords or `ajv: { strictSchema: 'log' }` to log warnings. This applies only to schema paths compiled by Ajv, not to every schema processed by fast-json-stringify.
|
|
131
131
|
- `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
|
|
132
132
|
- `maxDepth`: maximum number of nested schema levels allowed during compilation.
|
|
133
133
|
Defaults to `100` and may be set from `0` to `100`. Schemas that exceed this limit
|
package/index.js
CHANGED
|
@@ -13,7 +13,7 @@ const mergeSchemas = require('./lib/merge-schemas')
|
|
|
13
13
|
const DEFAULT_MAX_DEPTH = 100
|
|
14
14
|
const NAMED_FRAGMENT_REF = /^#[a-z_][-\w._]*$/i
|
|
15
15
|
|
|
16
|
-
// Minimum array lengths at which copying into a
|
|
16
|
+
// Minimum array lengths at which copying into a plain array and
|
|
17
17
|
// calling native JSON.stringify beats the generated per-item loop. Measured
|
|
18
18
|
// against main with tinybench (alternating rounds, median ops/s, output
|
|
19
19
|
// consumed with Buffer.byteLength) on Node 20, 22, 24 and 26 x64.
|
|
@@ -1055,7 +1055,10 @@ function buildArray (context, location, input) {
|
|
|
1055
1055
|
// Copying small arrays costs more than the existing loop.
|
|
1056
1056
|
if (typeof arrayLength === 'number' && arrayLength >= ${fastPath.minLength} && Number.isInteger(arrayLength) && arrayLength <= 0xffffffff) {
|
|
1057
1057
|
const copy = new Array(arrayLength)
|
|
1058
|
-
|
|
1058
|
+
// V8's side-effect-free JSON.stringify fast path (V8 >= 13.8) requires
|
|
1059
|
+
// the standard Array.prototype; a null prototype halves its throughput.
|
|
1060
|
+
// Only detach the prototype when a toJSON hook would otherwise be called.
|
|
1061
|
+
if (Array.prototype.toJSON !== undefined) Object.setPrototypeOf(copy, null)
|
|
1059
1062
|
for (let i = 0; i < arrayLength; i++) {
|
|
1060
1063
|
const v = obj[i]
|
|
1061
1064
|
if (!(${fastPath.condition})) {
|
package/lib/serializer.js
CHANGED
|
@@ -140,7 +140,10 @@ module.exports = class Serializer {
|
|
|
140
140
|
// matching buildSingleTypeSerializer without rereading getters.
|
|
141
141
|
asPlainStringArray (arr, len = arr.length) {
|
|
142
142
|
const copy = new Array(len)
|
|
143
|
-
|
|
143
|
+
// V8's side-effect-free JSON.stringify fast path (V8 >= 13.8) requires the
|
|
144
|
+
// standard Array.prototype; a null prototype halves its throughput. Only
|
|
145
|
+
// detach the prototype when a toJSON hook would otherwise be called.
|
|
146
|
+
if (Array.prototype.toJSON !== undefined) Object.setPrototypeOf(copy, null)
|
|
144
147
|
for (let i = 0; i < len; i++) {
|
|
145
148
|
const value = arr[i]
|
|
146
149
|
if (typeof value !== 'string') {
|
package/lib/validator.js
CHANGED
|
@@ -15,7 +15,7 @@ class Validator {
|
|
|
15
15
|
this.ajv = new Ajv({
|
|
16
16
|
...ajvOptions,
|
|
17
17
|
...codeOptions,
|
|
18
|
-
strictSchema: false,
|
|
18
|
+
strictSchema: ajvOptions?.strictSchema ?? false,
|
|
19
19
|
validateSchema: false,
|
|
20
20
|
allowUnionTypes: true,
|
|
21
21
|
uriResolver: fastUri
|
|
@@ -50,7 +50,7 @@ class Validator {
|
|
|
50
50
|
const ajvSchema = clone(schema)
|
|
51
51
|
this.convertSchemaToAjvFormat(ajvSchema)
|
|
52
52
|
this.ajv.addSchema(ajvSchema, schemaKey)
|
|
53
|
-
this._ajvSchemas[schemaKey] =
|
|
53
|
+
this._ajvSchemas[schemaKey] = ajvSchema
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
// The copy-then-JSON.stringify fast path for primitive arrays must produce the
|
|
7
|
+
// same output whether or not Array.prototype.toJSON has been patched, and must
|
|
8
|
+
// never invoke such a hook.
|
|
9
|
+
|
|
10
|
+
const numbers = Array.from({ length: 1000 }, (_, i) => i * 1.37)
|
|
11
|
+
const integers = Array.from({ length: 1000 }, (_, i) => i)
|
|
12
|
+
const strings = Array.from({ length: 1000 }, (_, i) => 'item-' + i)
|
|
13
|
+
|
|
14
|
+
const cases = [
|
|
15
|
+
['number', { type: 'array', items: { type: 'number' } }, numbers, {}],
|
|
16
|
+
['integer', { type: 'array', items: { type: 'integer' } }, integers, {}],
|
|
17
|
+
['string', { type: 'array', items: { type: 'string' } }, strings, { largeArraySize: 100 }]
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
for (const [name, schema, data, options] of cases) {
|
|
21
|
+
test(`${name} array fast path with pristine Array.prototype`, t => {
|
|
22
|
+
t.plan(2)
|
|
23
|
+
t.assert.equal(Array.prototype.toJSON, undefined)
|
|
24
|
+
const stringify = build(schema, options)
|
|
25
|
+
t.assert.equal(stringify(data), JSON.stringify(data))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test(`${name} array fast path ignores a patched Array.prototype.toJSON`, t => {
|
|
29
|
+
t.plan(2)
|
|
30
|
+
const expected = JSON.stringify(data)
|
|
31
|
+
let called = 0
|
|
32
|
+
// eslint-disable-next-line no-extend-native
|
|
33
|
+
Array.prototype.toJSON = function () { called++; return 'hooked' }
|
|
34
|
+
t.after(() => { delete Array.prototype.toJSON })
|
|
35
|
+
const stringify = build(schema, options)
|
|
36
|
+
t.assert.equal(stringify(data), expected)
|
|
37
|
+
t.assert.equal(called, 0)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test(`${name} array fast path resumes per-item coercion after a miss, with patched toJSON`, t => {
|
|
41
|
+
t.plan(1)
|
|
42
|
+
const mixed = data.slice()
|
|
43
|
+
mixed[500] = name === 'string' ? 42 : '7'
|
|
44
|
+
const stringify = build(schema, options)
|
|
45
|
+
const expected = stringify(mixed)
|
|
46
|
+
// eslint-disable-next-line no-extend-native
|
|
47
|
+
Array.prototype.toJSON = function () { return 'hooked' }
|
|
48
|
+
t.after(() => { delete Array.prototype.toJSON })
|
|
49
|
+
t.assert.equal(build(schema, options)(mixed), expected)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
@@ -318,6 +318,38 @@ test('inline validators with external oneOf refs and toJSON values', async (t) =
|
|
|
318
318
|
t.assert.throws(() => stringify({ value: true }), /does not match schema definition/)
|
|
319
319
|
})
|
|
320
320
|
|
|
321
|
+
test('restored validator handles toJSON values', async (t) => {
|
|
322
|
+
t.plan(3)
|
|
323
|
+
|
|
324
|
+
after(async () => {
|
|
325
|
+
await fs.promises.rm(destination, { force: true })
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
const code = fjs({
|
|
329
|
+
type: 'object',
|
|
330
|
+
properties: {
|
|
331
|
+
value: {
|
|
332
|
+
oneOf: [
|
|
333
|
+
{ type: 'string', format: 'date-time' },
|
|
334
|
+
{ type: 'integer' }
|
|
335
|
+
]
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}, { mode: 'standalone', inlineValidators: false })
|
|
339
|
+
|
|
340
|
+
const destination = path.resolve(tmpDir, 'standalone-restored-toJSON.js')
|
|
341
|
+
|
|
342
|
+
await fs.promises.writeFile(destination, code)
|
|
343
|
+
const stringify = require(destination)
|
|
344
|
+
|
|
345
|
+
t.assert.equal(
|
|
346
|
+
stringify({ value: new Date('2020-01-02T03:04:05.000Z') }),
|
|
347
|
+
'{"value":"2020-01-02T03:04:05.000Z"}'
|
|
348
|
+
)
|
|
349
|
+
t.assert.equal(stringify({ value: 42 }), '{"value":42}')
|
|
350
|
+
t.assert.throws(() => stringify({ value: true }), /does not match schema definition/)
|
|
351
|
+
})
|
|
352
|
+
|
|
321
353
|
test('does not emit unused inline validators', (t) => {
|
|
322
354
|
const code = build({ mode: 'standalone', inlineValidators: true })
|
|
323
355
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
function schemaWithTypo () {
|
|
7
|
+
return { anyOf: [{ type: 'number', mininum: 10 }] }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
for (const compileValidators of [false, true]) {
|
|
11
|
+
const mode = compileValidators ? 'eager' : 'lazy'
|
|
12
|
+
|
|
13
|
+
for (const [name, ajv] of [
|
|
14
|
+
['omitted', undefined],
|
|
15
|
+
['empty', {}],
|
|
16
|
+
['undefined', { strictSchema: undefined }],
|
|
17
|
+
['null', { strictSchema: null }],
|
|
18
|
+
['false', { strictSchema: false }],
|
|
19
|
+
['strict true', { strict: true }],
|
|
20
|
+
['explicit false with strict true', { strict: true, strictSchema: false }]
|
|
21
|
+
]) {
|
|
22
|
+
test(`strictSchema keeps permissive behavior for ${name} (${mode})`, t => {
|
|
23
|
+
const stringify = build(schemaWithTypo(), { ajv, compileValidators })
|
|
24
|
+
t.assert.equal(stringify(1), '1')
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test(`strictSchema true rejects unknown keywords (${mode})`, t => {
|
|
29
|
+
const options = { ajv: { strictSchema: true }, compileValidators }
|
|
30
|
+
const unknownKeyword = /unknown keyword: "mininum"/
|
|
31
|
+
|
|
32
|
+
if (compileValidators) {
|
|
33
|
+
t.assert.throws(() => build(schemaWithTypo(), options), unknownKeyword)
|
|
34
|
+
} else {
|
|
35
|
+
const stringify = build(schemaWithTypo(), options)
|
|
36
|
+
t.assert.throws(() => stringify(1), unknownKeyword)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test(`strictSchema log warns about unknown keywords (${mode})`, t => {
|
|
41
|
+
const warnings = []
|
|
42
|
+
const logger = {
|
|
43
|
+
log () {},
|
|
44
|
+
error () {},
|
|
45
|
+
warn (message) { warnings.push(message) }
|
|
46
|
+
}
|
|
47
|
+
const stringify = build(schemaWithTypo(), {
|
|
48
|
+
ajv: { strictSchema: 'log', logger },
|
|
49
|
+
compileValidators
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
t.assert.equal(stringify(1), '1')
|
|
53
|
+
t.assert.ok(warnings.some(message => message.includes('unknown keyword: "mininum"')))
|
|
54
|
+
})
|
|
55
|
+
}
|