diff --git a/README.md b/README.md index dd042d73..7ca3fdfe 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ const stringify = fastJson(mySchema, { ``` - `schema`: external schemas references by $ref property. [More details](#ref) -- `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof) +- `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. - `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer) - `maxDepth`: maximum number of nested schema levels allowed during compilation. Defaults to `100` and may be set from `0` to `100`. Schemas that exceed this limit diff --git a/lib/validator.js b/lib/validator.js index 124b5d59..57e053e3 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -15,7 +15,7 @@ class Validator { this.ajv = new Ajv({ ...ajvOptions, ...codeOptions, - strictSchema: false, + strictSchema: ajvOptions?.strictSchema ?? false, validateSchema: false, allowUnionTypes: true, uriResolver: fastUri diff --git a/test/strict-schema.test.js b/test/strict-schema.test.js new file mode 100644 index 00000000..0c69f65f --- /dev/null +++ b/test/strict-schema.test.js @@ -0,0 +1,55 @@ +'use strict' + +const { test } = require('node:test') +const build = require('..') + +function schemaWithTypo () { + return { anyOf: [{ type: 'number', mininum: 10 }] } +} + +for (const compileValidators of [false, true]) { + const mode = compileValidators ? 'eager' : 'lazy' + + for (const [name, ajv] of [ + ['omitted', undefined], + ['empty', {}], + ['undefined', { strictSchema: undefined }], + ['null', { strictSchema: null }], + ['false', { strictSchema: false }], + ['strict true', { strict: true }], + ['explicit false with strict true', { strict: true, strictSchema: false }] + ]) { + test(`strictSchema keeps permissive behavior for ${name} (${mode})`, t => { + const stringify = build(schemaWithTypo(), { ajv, compileValidators }) + t.assert.equal(stringify(1), '1') + }) + } + + test(`strictSchema true rejects unknown keywords (${mode})`, t => { + const options = { ajv: { strictSchema: true }, compileValidators } + const unknownKeyword = /unknown keyword: "mininum"/ + + if (compileValidators) { + t.assert.throws(() => build(schemaWithTypo(), options), unknownKeyword) + } else { + const stringify = build(schemaWithTypo(), options) + t.assert.throws(() => stringify(1), unknownKeyword) + } + }) + + test(`strictSchema log warns about unknown keywords (${mode})`, t => { + const warnings = [] + const logger = { + log () {}, + error () {}, + warn (message) { warnings.push(message) } + } + const stringify = build(schemaWithTypo(), { + ajv: { strictSchema: 'log', logger }, + compileValidators + }) + + t.assert.equal(stringify(1), '1') + t.assert.ok(warnings.some(message => message.includes('unknown keyword: "mininum"'))) + }) +}