Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "schemaPath" to verbose output, showing which subschema triggered each error. #148

Merged
merged 2 commits into from
Dec 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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
Expand Down
37 changes: 25 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
}
Expand Down Expand Up @@ -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('}')
}
}
Expand Down Expand Up @@ -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('}')
}
})
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {')
Expand All @@ -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('}')
Expand All @@ -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('}')
})

Expand All @@ -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]))
})
}

Expand Down Expand Up @@ -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('}')
})
Expand All @@ -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')
Expand Down
147 changes: 147 additions & 0 deletions test/schema-path.js
Original file line number Diff line number Diff line change
@@ -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()
})