diff --git a/index.js b/index.js index 20dd386b..881b924f 100644 --- a/index.js +++ b/index.js @@ -174,6 +174,27 @@ function build (schema, options) { } } + // we've added the schemas in 'validatorSchemasIds' to the validator.ajv + // (these schemas will actually be used during serialisation of if-else / + // allOf / oneOf constructs that require matching against a schema before + // serialisation), however OTHER referenced schemas (those not actually + // required during serialisation) have not been added to validator.ajv, so + // we cannot use the validator's AJV instance to actually validate the + // whole schema. Instead, create a new AJV instance with the same options, + // add all the referenced schemas, and then validate: + const ajv = Validator.createAJVInstanceWithOptions(options.ajv) + + if (options.schema) { + for (const key in options.schema) { + const schema = options.schema[key] + ajv.addSchema(schema, getSchemaId(schema, key)) + } + } + // compile the root schema with AJV (even though we don't use the result): + // this ensures early and consistent detection of errors in the schema + // based on the AJV options (for example options.ajv.strictSchema): + ajv.compile(schema) + if (options.debugMode) { options.mode = 'debug' } diff --git a/lib/validator.js b/lib/validator.js index 26c93f28..09e891b6 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -7,25 +7,7 @@ const clone = require('rfdc')({ proto: true }) class Validator { constructor (ajvOptions) { - this.ajv = new Ajv({ - ...ajvOptions, - strictSchema: false, - validateSchema: false, - allowUnionTypes: true, - uriResolver: fastUri - }) - - ajvFormats(this.ajv) - - this.ajv.addKeyword({ - keyword: 'fjs_type', - type: 'object', - errors: false, - validate: (type, date) => { - return date instanceof Date - } - }) - + this.ajv = Validator.createAJVInstanceWithOptions(ajvOptions) this._ajvSchemas = {} this._ajvOptions = ajvOptions || {} } @@ -82,6 +64,29 @@ class Validator { } } + static createAJVInstanceWithOptions (ajvOptions) { + const ajv = new Ajv({ + strictSchema: false, + validateSchema: false, + allowUnionTypes: true, + uriResolver: fastUri, + ...ajvOptions + }) + + ajvFormats(ajv) + + ajv.addKeyword({ + keyword: 'fjs_type', + type: 'object', + errors: false, + validate: (type, date) => { + return date instanceof Date + } + }) + + return ajv + } + static restoreFromState (state) { const validator = new Validator(state.ajvOptions) for (const [id, ajvSchema] of Object.entries(state.ajvSchemas)) { diff --git a/test/strictSchema.test.js b/test/strictSchema.test.js new file mode 100644 index 00000000..1ffa8e4b --- /dev/null +++ b/test/strictSchema.test.js @@ -0,0 +1,44 @@ +'use strict' + +const test = require('tap').test +const build = require('..') + +test('throw on unknown keyword with strictSchema: true', (t) => { + t.plan(1) + + const schema = { + type: 'object', + properties: { + a: { type: 'string' } + }, + anUnknownKeyword: true + } + + t.throws(() => { + build(schema, { ajv: { strictSchema: true } }) + }, new Error('strict mode: unknown keyword: "anUnknownKeyword"')) +}) + +test('throw on unknown keyword in a referenced schema with strictSchema: true', (t) => { + t.plan(1) + + const referencedSchemas = { + foo: { + properties: { + b: { type: 'string' } + }, + anUnknownKeyword: true + } + } + + const schema = { + type: 'object', + properties: { + a: { type: 'object', $ref: 'foo' } + } + } + + t.throws(() => { + build(schema, { schema: referencedSchemas, ajv: { strictSchema: true } }) + }, new Error('strict mode: unknown keyword: "anUnknownKeyword"')) +})