From c081061a1e6a998b65bd11aba7a0dc25b42d207d Mon Sep 17 00:00:00 2001 From: Teddy Katz Date: Fri, 1 Mar 2019 02:19:12 -0500 Subject: [PATCH] feat: invalidDefaults option to warn when defaults are ignored, fixes #957 --- README.md | 8 ++- lib/ajv.d.ts | 1 + lib/ajv.js | 2 +- lib/dot/defaults.def | 30 +++++++---- lib/dot/validate.jst | 9 +++- spec/options/useDefaults.spec.js | 88 +++++++++++++++++++++++++++++++- 6 files changed, 124 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5ddefa134..c26aa078d 100644 --- a/README.md +++ b/README.md @@ -798,13 +798,14 @@ console.log(validate(data)); // true console.log(data); // [ 1, "foo" ] ``` -`default` keywords in other cases are ignored: +`default` keywords in other cases are invalid: - not in `properties` or `items` subschemas - in schemas inside `anyOf`, `oneOf` and `not` (see [#42](https://github.com/epoberezkin/ajv/issues/42)) - in `if` subschema of `switch` keyword - in schemas generated by custom macro keywords +The [`invalidDefaults` option](#options) customizes Ajv's behavior for invalid defaults (`false` ignores invalid defaults, `true` raises an error, and `"log"` outputs a warning). ## Coercing data types @@ -1070,6 +1071,7 @@ Defaults: removeAdditional: false, useDefaults: false, coerceTypes: false, + invalidDefaults: false, // asynchronous validation options: transpile: undefined, // requires ajv-async package // advanced options: @@ -1151,6 +1153,10 @@ Defaults: - `false` (default) - no type coercion. - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). +- _invalidDefaults_: specify behavior for invalid `default` keywords in schemas. Option values: + - `false` (default) - ignore invalid defaults + - `true` - if an invalid default is present, throw an error + - `"log"` - if an invalid default is present, log warning ##### Asynchronous validation options diff --git a/lib/ajv.d.ts b/lib/ajv.d.ts index 8b0d9ab6d..bbba7a593 100644 --- a/lib/ajv.d.ts +++ b/lib/ajv.d.ts @@ -180,6 +180,7 @@ declare namespace ajv { removeAdditional?: boolean | 'all' | 'failing'; useDefaults?: boolean | 'shared'; coerceTypes?: boolean | 'array'; + invalidDefaults?: boolean | 'log'; async?: boolean | string; transpile?: string | ((code: string) => string); meta?: boolean | object; diff --git a/lib/ajv.js b/lib/ajv.js index 105315adb..0b975a28f 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -39,7 +39,7 @@ Ajv.$dataMetaSchema = $dataMetaSchema; var META_SCHEMA_ID = 'http://json-schema.org/draft-07/schema'; -var META_IGNORE_OPTIONS = [ 'removeAdditional', 'useDefaults', 'coerceTypes' ]; +var META_IGNORE_OPTIONS = [ 'removeAdditional', 'useDefaults', 'coerceTypes', 'invalidDefaults' ]; var META_SUPPORT_DATA = ['/properties']; /** diff --git a/lib/dot/defaults.def b/lib/dot/defaults.def index f100cc4bf..3e4a8de00 100644 --- a/lib/dot/defaults.def +++ b/lib/dot/defaults.def @@ -1,15 +1,25 @@ {{## def.assignDefault: - if ({{=$passData}} === undefined - {{? it.opts.useDefaults == 'empty' }} - || {{=$passData}} === null - || {{=$passData}} === '' + {{? it.compositeRule }} + {{? it.opts.invalidDefaults }} + {{? it.opts.invalidDefaults === 'log' }} + {{ it.logger.warn('default is ignored for: ' + $passData); }} + {{??}} + {{ throw new Error('default is ignored for: ' + $passData); }} + {{?}} {{?}} - ) - {{=$passData}} = {{? it.opts.useDefaults == 'shared' }} - {{= it.useDefault($sch.default) }} - {{??}} - {{= JSON.stringify($sch.default) }} - {{?}}; + {{??}} + if ({{=$passData}} === undefined + {{? it.opts.useDefaults == 'empty' }} + || {{=$passData}} === null + || {{=$passData}} === '' + {{?}} + ) + {{=$passData}} = {{? it.opts.useDefaults == 'shared' }} + {{= it.useDefault($sch.default) }} + {{??}} + {{= JSON.stringify($sch.default) }} + {{?}}; + {{?}} #}} diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index 89a5b3b49..4e05ce890 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -72,6 +72,13 @@ it.dataPathArr = [undefined]; }} + {{? it.opts.invalidDefaults && it.schema.default !== undefined }} + {{? it.opts.invalidDefaults === 'log' }} + {{ it.logger.warn('default is ignored in the schema root'); }} + {{??}} + {{ throw new Error('default is ignored in the schema root'); }} + {{?}} + {{?}} var vErrors = null; {{ /* don't edit, used in replace */ }} var errors = 0; {{ /* don't edit, used in replace */ }} @@ -177,7 +184,7 @@ {{? $rulesGroup.type }} if ({{= it.util.checkDataType($rulesGroup.type, $data) }}) { {{?}} - {{? it.opts.useDefaults && !it.compositeRule }} + {{? it.opts.useDefaults }} {{? $rulesGroup.type == 'object' && it.schema.properties }} {{# def.defaultProperties }} {{?? $rulesGroup.type == 'array' && Array.isArray(it.schema.items) }} diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.js index 7a12e8423..4570a326c 100644 --- a/spec/options/useDefaults.spec.js +++ b/spec/options/useDefaults.spec.js @@ -2,7 +2,7 @@ var Ajv = require('../ajv'); var getAjvInstances = require('../ajv_instances'); -require('../chai').should(); +var should = require('../chai').should(); describe('useDefaults options', function() { @@ -220,4 +220,90 @@ describe('useDefaults options', function() { }); }); }); + + describe('invalidDefaults option', function() { + it('should throw an error given an invalid default in the schema root when invalidDefaults is true', function() { + var ajv = new Ajv({useDefaults: true, invalidDefaults: true}); + var schema = { + default: 5, + properties: {} + }; + should.throw(function() { ajv.compile(schema); }); + }); + + it('should throw an error given an invalid default in oneOf when invalidDefaults is true', function() { + var ajv = new Ajv({useDefaults: true, invalidDefaults: true}); + var schema = { + oneOf: [ + { enum: ['foo', 'bar'] }, + { + properties: { + foo: { + default: true + } + } + } + ] + }; + should.throw(function() { ajv.compile(schema); }); + }); + + it('should log a warning given an invalid default in the schema root when invalidDefaults is "log"', function() { + var warnArg = null; + var ajv = new Ajv({ + useDefaults: true, + invalidDefaults: 'log', + logger: { + log: function() { + throw new Error('should not be called'); + }, + warn: function(warning) { + warnArg = warning; + }, + error: function() { + throw new Error('should not be called'); + } + } + }); + var schema = { + default: 5, + properties: {} + }; + ajv.compile(schema); + should.equal(warnArg, 'default is ignored in the schema root'); + }); + + it('should log a warning given an invalid default in oneOf when invalidDefaults is "log"', function() { + var warnArg = null; + var ajv = new Ajv({ + useDefaults: true, + invalidDefaults: 'log', + logger: { + log: function() { + throw new Error('should not be called'); + }, + warn: function(warning) { + warnArg = warning; + }, + error: function() { + throw new Error('should not be called'); + } + } + }); + var schema = { + oneOf: [ + { enum: ['foo', 'bar'] }, + { + properties: { + foo: { + default: true + } + } + } + ] + }; + ajv.compile(schema); + should.equal(warnArg, 'default is ignored for: data.foo'); + }); + }); });