Skip to content

Commit

Permalink
Merge pull request #148 from deBhal/add/schemaPath
Browse files Browse the repository at this point in the history
Add "schemaPath" to verbose output, showing which subschema triggered each error.
  • Loading branch information
LinusU authored Dec 18, 2017
2 parents 2190955 + 1edfc55 commit 928417d
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 17 deletions.
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()
})

0 comments on commit 928417d

Please sign in to comment.