From 566a67d4f996cfeb009b05e7284d36336a48dabe Mon Sep 17 00:00:00 2001 From: James Crosby Date: Wed, 10 Jan 2024 16:46:25 +0000 Subject: [PATCH 1/3] Set strictSchema by default to make silent errors less likely, and allow ajv options to be overriden --- lib/validator.js | 6 +++--- test/anyof.test.js | 2 +- test/array.test.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/validator.js b/lib/validator.js index 26c93f28..761955d0 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -8,11 +8,11 @@ const clone = require('rfdc')({ proto: true }) class Validator { constructor (ajvOptions) { this.ajv = new Ajv({ - ...ajvOptions, - strictSchema: false, + strictSchema: true, validateSchema: false, allowUnionTypes: true, - uriResolver: fastUri + uriResolver: fastUri, + ...ajvOptions }) ajvFormats(this.ajv) diff --git a/test/anyof.test.js b/test/anyof.test.js index 03483329..1d0c0e7a 100644 --- a/test/anyof.test.js +++ b/test/anyof.test.js @@ -218,7 +218,7 @@ test('symbol value in schema', (t) => { required: ['value'] } - const stringify = build(schema) + const stringify = build(schema, { ajv: { strictSchema: false } }) t.equal(stringify({ value: 'foo' }), '{"value":"foo"}') t.equal(stringify({ value: 'bar' }), '{"value":"bar"}') t.equal(stringify({ value: 'baz' }), '{"value":"baz"}') diff --git a/test/array.test.js b/test/array.test.js index fbffe02f..cc6dcba1 100644 --- a/test/array.test.js +++ b/test/array.test.js @@ -397,7 +397,7 @@ test('object array with anyOf and symbol', (t) => { required: ['name', 'option'] } } - const stringify = build(schema) + const stringify = build(schema, { ajv: { strictSchema: false } }) const value = stringify([ { name: 'name-0', option: 'Foo' }, { name: 'name-1', option: 'Bar' } From 7fb52bb4f8f0f5dbca9d826ceb3c6293c5286ff4 Mon Sep 17 00:00:00 2001 From: James Crosby Date: Fri, 12 Jan 2024 15:03:06 +0000 Subject: [PATCH 2/3] To actually validate the schema we need to explicitly compile it; keep the default value of strictSchema (but still allow it to be overriden) --- index.js | 38 +++++++++++++++++++++++++++++++++ lib/validator.js | 2 +- test/anyof.test.js | 2 +- test/array.test.js | 2 +- test/strictSchema.test.js | 44 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 test/strictSchema.test.js diff --git a/index.js b/index.js index 20dd386b..55d05feb 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,9 @@ const merge = require('@fastify/deepmerge')() const clone = require('rfdc')({ proto: true }) const { RefResolver } = require('json-schema-ref-resolver') +const Ajv = require('ajv') +const fastUri = require('fast-uri') +const ajvFormats = require('ajv-formats') const validate = require('./lib/schema-validator') const Serializer = require('./lib/serializer') @@ -174,6 +177,41 @@ 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 = new Ajv({ + strictSchema: false, + validateSchema: false, + allowUnionTypes: true, + uriResolver: fastUri, + ...options.ajv + }) + ajvFormats(ajv) + ajv.addKeyword({ + keyword: 'fjs_type', + type: 'object', + errors: false, + validate: (type, date) => { + return date instanceof Date + } + }) + 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 761955d0..e54a9d34 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -8,7 +8,7 @@ const clone = require('rfdc')({ proto: true }) class Validator { constructor (ajvOptions) { this.ajv = new Ajv({ - strictSchema: true, + strictSchema: false, validateSchema: false, allowUnionTypes: true, uriResolver: fastUri, diff --git a/test/anyof.test.js b/test/anyof.test.js index 1d0c0e7a..03483329 100644 --- a/test/anyof.test.js +++ b/test/anyof.test.js @@ -218,7 +218,7 @@ test('symbol value in schema', (t) => { required: ['value'] } - const stringify = build(schema, { ajv: { strictSchema: false } }) + const stringify = build(schema) t.equal(stringify({ value: 'foo' }), '{"value":"foo"}') t.equal(stringify({ value: 'bar' }), '{"value":"bar"}') t.equal(stringify({ value: 'baz' }), '{"value":"baz"}') diff --git a/test/array.test.js b/test/array.test.js index cc6dcba1..fbffe02f 100644 --- a/test/array.test.js +++ b/test/array.test.js @@ -397,7 +397,7 @@ test('object array with anyOf and symbol', (t) => { required: ['name', 'option'] } } - const stringify = build(schema, { ajv: { strictSchema: false } }) + const stringify = build(schema) const value = stringify([ { name: 'name-0', option: 'Foo' }, { name: 'name-1', option: 'Bar' } 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"')) +}) From 4db0f23a460232fcd2be2aebdf67e9f6a21e65d1 Mon Sep 17 00:00:00 2001 From: James Crosby Date: Fri, 12 Jan 2024 15:15:37 +0000 Subject: [PATCH 3/3] de-duplicate AJV initialisation --- index.js | 21 ++------------------- lib/validator.js | 43 ++++++++++++++++++++++++------------------- 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/index.js b/index.js index 55d05feb..881b924f 100644 --- a/index.js +++ b/index.js @@ -5,9 +5,6 @@ const merge = require('@fastify/deepmerge')() const clone = require('rfdc')({ proto: true }) const { RefResolver } = require('json-schema-ref-resolver') -const Ajv = require('ajv') -const fastUri = require('fast-uri') -const ajvFormats = require('ajv-formats') const validate = require('./lib/schema-validator') const Serializer = require('./lib/serializer') @@ -185,22 +182,8 @@ function build (schema, options) { // 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 = new Ajv({ - strictSchema: false, - validateSchema: false, - allowUnionTypes: true, - uriResolver: fastUri, - ...options.ajv - }) - ajvFormats(ajv) - ajv.addKeyword({ - keyword: 'fjs_type', - type: 'object', - errors: false, - validate: (type, date) => { - return date instanceof Date - } - }) + const ajv = Validator.createAJVInstanceWithOptions(options.ajv) + if (options.schema) { for (const key in options.schema) { const schema = options.schema[key] diff --git a/lib/validator.js b/lib/validator.js index e54a9d34..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({ - strictSchema: false, - validateSchema: false, - allowUnionTypes: true, - uriResolver: fastUri, - ...ajvOptions - }) - - 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)) {