diff --git a/README.md b/README.md index e3963f6..5ade478 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,15 @@ var doc = {hello: 'world', notInSchema: true} console.log(filter(doc)) // {hello: 'world'} ``` -## Verbose mode outputs the value on errors +## Verbose mode shows more information about the source of the error -is-my-json-valid outputs the value causing an error when verbose is set to true +When the `verbose` options is set to `true`, `is-my-json-valid` also outputs: + +- `value`: The data value that caused the error +- `schemaPath`: an array of keys indicating which sub-schema failed ``` js -var validate = validator({ +var schema = { required: true, type: 'object', properties: { @@ -123,12 +126,33 @@ var validate = validator({ type: 'string' } } -}, { +} +var validate = validator(schema, { verbose: true }) validate({hello: 100}); -console.log(validate.errors) // {field: 'data.hello', message: 'is the wrong type', value: 100, type: 'string'} +console.log(validate.errors) +// [ { field: 'data.hello', +// message: 'is the wrong type', +// value: 100, +// type: 'string', +// schemaPath: [ 'properties', 'hello' ] } ] +``` + +Many popular libraries make it easy to retrieve the failing rule with the `schemaPath`: +``` +var schemaPath = validate.errors[0].schemaPath +var R = require('ramda') + +console.log( 'All evaluate to the same thing: ', R.equals( + schema.properties.hello, + { required: true, type: 'string' }, + R.path(schemaPath, schema), + require('lodash').get(schema, schemaPath), + require('jsonpointer').get(schema, [""].concat(schemaPath)) +)) +// All evaluate to the same thing: true ``` ## Greedy mode tries to validate as much as possible diff --git a/index.js b/index.js index f8e1611..b80ba95 100644 --- a/index.js +++ b/index.js @@ -137,7 +137,7 @@ var compile = function(schema, cache, root, reporter, opts) { return v } - var visit = function(name, node, reporter, filter) { + var visit = function(name, node, reporter, filter, schemaPath) { var properties = node.properties var type = node.type var tuple = false @@ -157,7 +157,14 @@ var compile = function(schema, cache, root, reporter, opts) { if (reporter === true) { validate('if (validate.errors === null) validate.errors = []') if (verbose) { - validate('validate.errors.push({field:%s,message:%s,value:%s,type:%s})', formatName(prop || name), JSON.stringify(msg), value || name, JSON.stringify(type)) + validate( + 'validate.errors.push({field:%s,message:%s,value:%s,type:%s,schemaPath:%s})', + formatName(prop || name), + JSON.stringify(msg), + value || name, + JSON.stringify(type), + JSON.stringify(schemaPath) + ) } else { validate('validate.errors.push({field:%s,message:%s})', formatName(prop || name), JSON.stringify(msg)) } @@ -199,7 +206,7 @@ var compile = function(schema, cache, root, reporter, opts) { } else if (node.additionalItems) { var i = genloop() validate('for (var %s = %d; %s < %s.length; %s++) {', i, node.items.length, i, name, i) - visit(name+'['+i+']', node.additionalItems, reporter, filter) + visit(name+'['+i+']', node.additionalItems, reporter, filter, schemaPath.concat('additionalItems')) validate('}') } } @@ -278,7 +285,7 @@ var compile = function(schema, cache, root, reporter, opts) { } if (typeof deps === 'object') { validate('if (%s !== undefined) {', genobj(name, key)) - visit(name, deps, reporter, filter) + visit(name, deps, reporter, filter, schemaPath.concat(['dependencies', key])) validate('}') } }) @@ -312,7 +319,7 @@ var compile = function(schema, cache, root, reporter, opts) { if (filter) validate('delete %s', name+'['+keys+'['+i+']]') error('has additional properties', null, JSON.stringify(name+'.') + ' + ' + keys + '['+i+']') } else { - visit(name+'['+keys+'['+i+']]', node.additionalProperties, reporter, filter) + visit(name+'['+keys+'['+i+']]', node.additionalProperties, reporter, filter, schemaPath.concat(['additionalProperties'])) } validate @@ -343,7 +350,7 @@ var compile = function(schema, cache, root, reporter, opts) { if (node.not) { var prev = gensym('prev') validate('var %s = errors', prev) - visit(name, node.not, false, filter) + visit(name, node.not, false, filter, schemaPath.concat('not')) validate('if (%s === errors) {', prev) error('negative schema matches') validate('} else {') @@ -356,7 +363,7 @@ var compile = function(schema, cache, root, reporter, opts) { var i = genloop() validate('for (var %s = 0; %s < %s.length; %s++) {', i, i, name, i) - visit(name+'['+i+']', node.items, reporter, filter) + visit(name+'['+i+']', node.items, reporter, filter, schemaPath.concat('items')) validate('}') if (type !== 'array') validate('}') @@ -373,7 +380,7 @@ var compile = function(schema, cache, root, reporter, opts) { Object.keys(node.patternProperties).forEach(function(key) { var p = patterns(key) validate('if (%s.test(%s)) {', p, keys+'['+i+']') - visit(name+'['+keys+'['+i+']]', node.patternProperties[key], reporter, filter) + visit(name+'['+keys+'['+i+']]', node.patternProperties[key], reporter, filter, schemaPath.concat(['patternProperties', key])) validate('}') }) @@ -391,8 +398,8 @@ var compile = function(schema, cache, root, reporter, opts) { } if (node.allOf) { - node.allOf.forEach(function(sch) { - visit(name, sch, reporter, filter) + node.allOf.forEach(function(sch, key) { + visit(name, sch, reporter, filter, schemaPath.concat(['allOf', key])) }) } @@ -533,7 +540,13 @@ var compile = function(schema, cache, root, reporter, opts) { Object.keys(properties).forEach(function(p) { if (Array.isArray(type) && type.indexOf('null') !== -1) validate('if (%s !== null) {', name) - visit(genobj(name, p), properties[p], reporter, filter) + visit( + genobj(name, p), + properties[p], + reporter, + filter, + schemaPath.concat(tuple ? p : ['properties', p]) + ) if (Array.isArray(type) && type.indexOf('null') !== -1) validate('}') }) @@ -549,7 +562,7 @@ var compile = function(schema, cache, root, reporter, opts) { ('validate.errors = null') ('var errors = 0') - visit('data', schema, reporter, opts && opts.filter) + visit('data', schema, reporter, opts && opts.filter, []) validate ('return errors === 0') diff --git a/test/schema-path.js b/test/schema-path.js new file mode 100644 index 0000000..661c406 --- /dev/null +++ b/test/schema-path.js @@ -0,0 +1,147 @@ +var tape = require('tape') +var validator = require('../') +var get = require('jsonpointer').get; + +function toPointer( path ) { + if ( ! ( path && path.length && path.join ) ){ + return ''; + } + return '/' + path.join('/'); +} + +function lookup(schema, err){ + return get(schema, toPointer(err.schemaPath)); +} + +tape('schemaPath', function(t) { + var schema = { + type: 'object', + target: 'top level', + properties: { + target: 'inside properties', + hello: { + target: 'inside hello', + type:'string' + }, + someItems: { + target: 'in someItems', + type: 'array', + items: [ + { + type: 'string' + }, + { + type: 'array' + }, + ], + additionalItems: { + target: 'inside additionalItems', + type: 'boolean', + } + }, + nestedOuter: { + type: 'object', + target: 'in nestedOuter', + properties: { + nestedInner: { + type: 'object', + target: 'in nestedInner', + properties: { + deeplyNestedProperty: { + target: 'in deeplyNestedProperty', + type: "boolean" + } + } + }, + }, + required: ['nestedInner'] + }, + aggregate: { + allOf: [ + { pattern: 'z$' }, + { pattern: '^a' }, + { pattern: '-' }, + { pattern: '^...$' } + ] + }, + negate: { + target: "in negate", + not: { + type: "boolean" + } + }, + selection: { + target: 'in selection', + anyOf: [ + { 'pattern': '^[a-z]{3}$' }, + { 'pattern': '^[0-9]$' } + ], + }, + exclusiveSelection: { + target: 'There can be only one', + oneOf: [ + { pattern: 'a' }, + { pattern: 'e' }, + { pattern: 'i' }, + { pattern: 'o' }, + { pattern: 'u' } + ] + } + }, + patternProperties: { + ".*String": { type: 'string' }, + '^[01]+$': { type: 'number' } + }, + additionalProperties: false + } + var validate = validator(schema, { verbose: true, greedy: true } ); + + function notOkAt(data, path, message) { + if(validate(data)) { + return t.fail('should have failed: ' + message) + } + t.deepEqual(validate.errors[0].schemaPath, path, message) + } + + function notOkWithTarget(data, target, message) { + if(validate(data)) { + return t.fail('should have failed: ' + message) + } + t.deepEqual(lookup(schema, validate.errors[0]).target, target, message) + } + + // Top level errors + notOkAt(null, [], 'should target parent of failed type error') + notOkAt(undefined, [], 'should target parent of failed type error') + notOkWithTarget({invalidAdditionalProp: '*whistles innocently*'}, 'top level', 'additionalProperties should be associated with containing schema') + + // Errors in properties + notOkAt({hello: 42}, ['properties', 'hello'], 'should target property with type error') + notOkAt({someItems: [42]}, ['properties','someItems','0'], 'should target specific someItems rule(0)') + notOkAt({someItems: ['astring', 42]}, ['properties','someItems','1'], 'should target specific someItems rule(1)') + notOkAt({someItems: ['astring', 42, 'not a boolean']}, ['properties','someItems', 'additionalItems'], 'should target additionalItems') + notOkWithTarget({someItems: ['astring', 42, true, false, 42]}, 'inside additionalItems', 'should sitll target additionalProperties after valid additional items') + + notOkWithTarget({nestedOuter: {}}, 'in nestedOuter', 'should target container of missing required property') + notOkWithTarget({nestedOuter: {nestedInner: 'not an object'}}, 'in nestedInner', 'should target property with type error (inner)') + notOkWithTarget({nestedOuter: {nestedInner: {deeplyNestedProperty: 'not a boolean'}}}, 'in deeplyNestedProperty', 'should target property with type error (deep)') + + notOkAt({aggregate: 'a-a'}, ['properties', 'aggregate', 'allOf', 0], 'should target specific rule in allOf (0)') + notOkAt({aggregate: 'z-z'}, ['properties', 'aggregate', 'allOf', 1], 'should target specific rule in allOf (1)') + notOkAt({aggregate: 'a:z'}, ['properties', 'aggregate', 'allOf', 2], 'should target specific rule in allOf (2)') + notOkAt({aggregate: 'a--z'}, ['properties', 'aggregate', 'allOf', 3], 'should target specific rule in allOf (3)') + + notOkAt({'notAString': 42}, ['patternProperties', '.*String'], 'should target the specific pattern in patternProperties (wildcards)') + notOkAt({ + 'I am a String': 'I really am', + '001100111011000111100': "Don't stand around jabbering when you're in mortal danger" + }, ['patternProperties', '^[01]+$'], 'should target the specific pattern in patternProperties ("binary" keys)') + + notOkWithTarget({negate: false}, 'in negate', 'should target container of not') + + notOkWithTarget(({selection: 'grit'}), 'in selection', 'should target container for anyOf (no matches)'); + notOkWithTarget(({exclusiveSelection: 'fly'}), 'There can be only one', 'should target container for oneOf (no match)'); + notOkWithTarget(({exclusiveSelection: 'ice'}), 'There can be only one', 'should target container for oneOf (multiple matches)'); + + t.end() +})