├── .jshintignore ├── .gitignore ├── .gitmodules ├── .jshintrc ├── validators ├── null.js ├── boolean.js ├── index.js ├── deep-equal.js ├── string.js ├── number.js ├── array.js ├── object.js └── base.js ├── tests ├── plugins.test.js ├── bootstrap.js ├── json-schema-test-suite.test.js ├── validators │ ├── number.test.js │ └── base.test.js ├── refs │ ├── json-schema-draft-03.json │ └── json-schema-draft-04.json └── index.test.js ├── validation-result.js ├── package.json ├── LICENSE ├── validation-context.js ├── index.js └── README.md /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/json-schema-test-suite"] 2 | path = tests/json-schema-test-suite 3 | url = git@github.com:json-schema/JSON-Schema-Test-Suite.git 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "undef": true, 4 | "eqeqeq": true, 5 | "eqnull": false, 6 | "newcap": true, 7 | "nomen": true, 8 | "trailing": true, 9 | 10 | "predef": [ 11 | "describe", 12 | "it", 13 | "assert", 14 | "validate" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /validators/null.js: -------------------------------------------------------------------------------- 1 | function validateNull(context, subject, schema) { 2 | if(subject !== null) { 3 | context.addError('Failed type:null criteria', subject, schema); 4 | return false; 5 | } 6 | 7 | context.cleanSubject = subject; 8 | 9 | return true; 10 | } 11 | 12 | module.exports = validateNull; 13 | -------------------------------------------------------------------------------- /validators/boolean.js: -------------------------------------------------------------------------------- 1 | function validateBoolean(context, subject, schema) { 2 | if(typeof subject !== 'boolean') { 3 | context.addError('Failed type:boolean criteria', subject, schema); 4 | return false; 5 | } 6 | 7 | context.cleanSubject = subject; 8 | 9 | return true; 10 | } 11 | 12 | module.exports = validateBoolean; 13 | -------------------------------------------------------------------------------- /validators/index.js: -------------------------------------------------------------------------------- 1 | exports.types = { 2 | 'any': function() { return true; }, 3 | 'array': require('./array'), 4 | 'boolean': require('./boolean'), 5 | 'integer': require('./number'), 6 | 'null': require('./null'), 7 | 'number': require('./number'), 8 | 'object': require('./object'), 9 | 'string': require('./string') 10 | }; 11 | 12 | exports.deepEqual = require('./deep-equal'); 13 | 14 | // base cannot be required until other validators are added 15 | exports.base = require('./base'); 16 | -------------------------------------------------------------------------------- /tests/plugins.test.js: -------------------------------------------------------------------------------- 1 | var skeemas = require('../'); 2 | 3 | describe('Plugins', function() { 4 | it('should be called correctly', function() { 5 | var called = false; 6 | skeemas.use(function(protoValidator) { 7 | called = true; 8 | assert.isObject(protoValidator); 9 | }); 10 | assert.isTrue(called); 11 | }); 12 | 13 | it('should allow validator modifications', function() { 14 | skeemas.use(function(protoValidator) { 15 | protoValidator.testMethod = function() { 16 | assert.isFunction(this.validate); 17 | assert.isObject(this._refs); 18 | return this; 19 | }; 20 | }); 21 | 22 | var validator = skeemas(); 23 | assert.isFunction(validator.testMethod); 24 | assert.strictEqual(validator.testMethod(), validator); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/bootstrap.js: -------------------------------------------------------------------------------- 1 | var validator = require('../')(); 2 | 3 | validator 4 | .addRef('http://json-schema.org/draft-03/schema', require('./refs/json-schema-draft-03')) 5 | .addRef('http://json-schema.org/draft-04/schema', require('./refs/json-schema-draft-04')) 6 | .addRef('http://localhost:1234/integer.json', require('./json-schema-test-suite/remotes/integer.json')) 7 | .addRef('http://localhost:1234/name.json', require('./json-schema-test-suite/remotes/name.json')) 8 | .addRef('http://localhost:1234/subSchemas.json', require('./json-schema-test-suite/remotes/subSchemas.json')) 9 | .addRef('http://localhost:1234/folder/folderInteger.json', require('./json-schema-test-suite/remotes/folder/folderInteger.json')); 10 | 11 | global.validate = validator.validate.bind(validator); 12 | global.assert = require('chai').assert; 13 | -------------------------------------------------------------------------------- /validation-result.js: -------------------------------------------------------------------------------- 1 | function errorToString() { 2 | return this.message + ' (pointer: ' + this.context + ')'; 3 | } 4 | 5 | var protoValidationResult = { 6 | addError: function(message, subject, criteria, context) { 7 | this.errors.push({ 8 | message: message, 9 | context: context.path.join('/'), 10 | value: subject, 11 | criteria: criteria, 12 | toString: errorToString 13 | }); 14 | this.valid = false; 15 | return this; 16 | } 17 | }; 18 | 19 | module.exports = function(instance) { 20 | return Object.create(protoValidationResult, { 21 | instance: { enumerable:true, writable:false, value:instance }, 22 | cleanInstance: { enumerable:true, writable:true, value: undefined }, 23 | valid: { enumerable:true, writable:true, value:true }, 24 | errors: { enumerable:true, writable:false, value:[] } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skeemas", 3 | "version": "1.2.5", 4 | "description": "Lightweight JSON Schema valiation", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha -R dot --require tests/bootstrap.js tests/{**/,}/*.test.js", 8 | "lint": "jshint ." 9 | }, 10 | "keywords": [ 11 | "json-schema", 12 | "validate", 13 | "validation", 14 | "json", 15 | "schema" 16 | ], 17 | "author": "Matt Dunlap ", 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:Prestaul/skeemas.git" 22 | }, 23 | "devDependencies": { 24 | "chai": "^1.10.0", 25 | "glob": "^4.3.5", 26 | "jshint": "^2.6.0", 27 | "mocha": "^2.1.0" 28 | }, 29 | "dependencies": { 30 | "skeemas-json-pointer": "^1.0.0", 31 | "skeemas-json-refs": "^1.0.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /validators/deep-equal.js: -------------------------------------------------------------------------------- 1 | function getType(subject) { 2 | var type = typeof subject; 3 | 4 | if(type === 'object') { 5 | if(subject === null) return 'null'; 6 | if(Array.isArray(subject)) return 'array'; 7 | } 8 | return type; 9 | } 10 | 11 | function arrayEqual(a, b) { 12 | var i = a.length; 13 | 14 | if(i !== b.length) return false; 15 | 16 | while(i--) { 17 | if(!deepEqual(a[i], b[i])) return false; 18 | } 19 | 20 | return true; 21 | } 22 | 23 | function objectEqual(a, b) { 24 | var keys = Object.keys(a), 25 | i = keys.length; 26 | 27 | if(i !== Object.keys(b).length) return false; 28 | 29 | while(i--) { 30 | if(!deepEqual(a[keys[i]], b[keys[i]])) return false; 31 | } 32 | 33 | return true; 34 | } 35 | 36 | var deepEqual = module.exports = function(a, b) { 37 | if(a === b) return true; 38 | 39 | var t = getType(a); 40 | 41 | if(t !== getType(b)) return false; 42 | 43 | if(t === 'array') return arrayEqual(a, b); 44 | if(t === 'object') return objectEqual(a, b); 45 | 46 | return false; 47 | }; 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2009-2014 Kristopher Michael Kowal and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /validators/string.js: -------------------------------------------------------------------------------- 1 | var decode = require('punycode').ucs2.decode; 2 | 3 | 4 | function minLength(context, subject, schema) { 5 | if(decode(subject).length < schema.minLength) { 6 | context.addError('Failed "minLength" criteria', subject, schema); 7 | return false; 8 | } 9 | 10 | return true; 11 | } 12 | 13 | function maxLength(context, subject, schema) { 14 | if(decode(subject).length > schema.maxLength) { 15 | context.addError('Failed "maxLength" criteria', subject, schema); 16 | return false; 17 | } 18 | 19 | return true; 20 | } 21 | 22 | function pattern(context, subject, schema) { 23 | var strPattern = schema.pattern; 24 | 25 | if(!subject.match(strPattern)) { 26 | context.addError('Failed "pattern" criteria (' + strPattern + ')', subject, strPattern); 27 | return false; 28 | } 29 | 30 | return true; 31 | } 32 | 33 | 34 | 35 | function validateString(context, subject, schema) { 36 | if(typeof subject !== 'string') { 37 | context.addError('Failed type:string criteria', schema); 38 | return false; 39 | } 40 | 41 | context.cleanSubject = subject; 42 | 43 | return context.runValidations([ 44 | [ 'minLength' in schema, minLength ], 45 | [ 'maxLength' in schema, maxLength ], 46 | [ 'pattern' in schema, pattern ] 47 | ], subject, schema); 48 | } 49 | 50 | module.exports = validateString; 51 | -------------------------------------------------------------------------------- /tests/json-schema-test-suite.test.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob'); 2 | 3 | /** 4 | * Load up the tests from the JSON Schema Test Suite (https://github.com/json-schema/JSON-Schema-Test-Suite) 5 | */ 6 | describe('JSON Schema Test Suite -', function() { 7 | function addTests(description, files) { 8 | describe(description, function() { 9 | files.forEach(function(file) { 10 | // Skip "zeroTerminatedFloats" because in javascript 1 === 1.0 11 | if(/zeroTerminatedFloats\.json$/.test(file)) return; 12 | 13 | // Load the suite 14 | require(file).forEach(function(suite) { 15 | describe(suite.description, function() { 16 | // Load the tests 17 | suite.tests.forEach(function(test) { 18 | // Create individual tests 19 | it(test.description, function() { 20 | var result = validate(test.data, suite.schema); 21 | assert.strictEqual(result.valid, test.valid, test.valid ? 'validates the instance' : 'invalidates the instance'); 22 | }); 23 | 24 | }); 25 | }); 26 | }); 27 | }); 28 | }); 29 | } 30 | 31 | var draft3 = glob.sync('./json-schema-test-suite/tests/draft3/{**/,}*.json', { cwd:__dirname }); 32 | addTests('draft3:', draft3); 33 | 34 | var draft4 = glob.sync('./json-schema-test-suite/tests/draft4/{**/,}*.json', { cwd:__dirname }); 35 | addTests('draft4:', draft4); 36 | }); 37 | -------------------------------------------------------------------------------- /validation-context.js: -------------------------------------------------------------------------------- 1 | var validationResult = require('./validation-result'), 2 | jsonRefs = require('skeemas-json-refs'); 3 | 4 | var protoContext = { 5 | addError: function(message, subject, criteria) { 6 | if(!this.silent) this.result.addError(message, subject, criteria, this); 7 | return this; 8 | }, 9 | silently: function(fn) { 10 | this.silent = true; 11 | var result = fn(); 12 | this.silent = false; 13 | return result; 14 | }, 15 | subcontext: function(schema) { 16 | return makeContext(schema, this, this.silent); 17 | }, 18 | runValidations: function(validations, subject, schema) { 19 | var breakOnError = this.breakOnError, 20 | args = Array.prototype.slice.call(arguments), 21 | valid = true, 22 | validation; 23 | 24 | args[0] = this; 25 | 26 | for(var i = 0, len = validations.length; i < len; i++) { 27 | validation = validations[i]; 28 | if(!validation[0]) continue; 29 | valid = validation[1].apply(null, args) && valid; 30 | if(breakOnError && !valid) return false; 31 | } 32 | 33 | return valid; 34 | } 35 | }; 36 | 37 | var makeContext = module.exports = function(schema, context, forceNewResult) { 38 | context = context || {}; 39 | return Object.create(protoContext, { 40 | id: { enumerable:true, writable:false, value: [] }, 41 | schema: { enumerable:true, writable:false, value: schema || context.schema }, 42 | path: { enumerable:true, writable:false, value: context.path && context.path.slice() || ['#'] }, 43 | result: { enumerable:true, writable:false, value: (!forceNewResult && context.result) || validationResult(context.instance) }, 44 | refs: { enumerable:true, writable:false, value: context.refs || jsonRefs() }, 45 | silent: { enumerable:true, writable:true, value: false }, 46 | breakOnError: { enumerable:true, writable:true, value: context.breakOnError || false }, 47 | cleanWithDefaults: { enumerable:true, writable:true, value: context.cleanWithDefaults || false }, 48 | cleanSubject: { enumerable:true, writable:true, value: undefined } 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /validators/number.js: -------------------------------------------------------------------------------- 1 | function validateNumber(context, subject, schema) { 2 | if(typeof subject !== 'number') { 3 | context.addError('Failed type:number criteria', subject, schema); 4 | return false; 5 | } 6 | 7 | return true; 8 | } 9 | 10 | function validateInteger(context, subject, schema) { 11 | if(typeof subject !== 'number' || subject !== Math.round(subject)) { 12 | context.addError('Failed type:integer criteria', subject, schema); 13 | return false; 14 | } 15 | 16 | return true; 17 | } 18 | 19 | function minimum(context, subject, schema) { 20 | var valid = (schema.exclusiveMinimum) ? subject > schema.minimum : subject >= schema.minimum; 21 | 22 | if(!valid) context.addError('Failed "minimum" criteria', subject, schema); 23 | 24 | return valid; 25 | } 26 | 27 | function maximum(context, subject, schema) { 28 | var valid = (schema.exclusiveMaximum) ? subject < schema.maximum : subject <= schema.maximum; 29 | 30 | if(!valid) context.addError('Failed "maximum" criteria', subject, schema); 31 | 32 | return valid; 33 | } 34 | 35 | function multipleOf(context, subject, schema, key) { 36 | key = key || 'multipleOf'; 37 | 38 | var valid = (subject / schema[key] % 1) === 0; 39 | 40 | if(!valid) context.addError('Failed "' + key + '" criteria', subject, schema); 41 | 42 | return valid; 43 | } 44 | 45 | function divisibleBy(context, subject, schema) { 46 | return multipleOf(context, subject, schema, 'divisibleBy'); 47 | } 48 | 49 | 50 | 51 | module.exports = function(context, subject, schema) { 52 | var valid = true, 53 | isType = true; 54 | 55 | if(schema.type === 'number') isType = validateNumber(context, subject, schema); 56 | if(schema.type === 'integer') isType = validateInteger(context, subject, schema); 57 | 58 | context.cleanSubject = subject; 59 | 60 | return isType && context.runValidations([ 61 | [ 'minimum' in schema, minimum ], 62 | [ 'maximum' in schema, maximum ], 63 | [ 'multipleOf' in schema, multipleOf ], 64 | [ 'divisibleBy' in schema, divisibleBy ] 65 | ], subject, schema); 66 | }; 67 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var validators = require('./validators'), 2 | validationContext = require('./validation-context'), 3 | jsonRefs = require('skeemas-json-refs'); 4 | 5 | var protoValidator = { 6 | addRef: function(uri, ref) { 7 | if(typeof uri === 'object') { 8 | ref = uri; 9 | uri = null; 10 | } 11 | uri = uri || ref.id; 12 | 13 | if(!uri) throw new Error('Cannot add a json schema reference without a uri/id.'); 14 | 15 | this._refs.add(uri, ref); 16 | 17 | return this; 18 | }, 19 | validate: function(instance, schema, options) { 20 | if(instance === undefined) throw new Error('Instance undefined in call to validate.'); 21 | if(!schema) throw new Error('No schema specified in call to validate.'); 22 | 23 | if(typeof schema === 'string') { 24 | var uri = schema; 25 | schema = this._refs.get(uri); 26 | 27 | if(!schema) throw new Error('Unable to locate schema (' + uri + '). Did you call addRef with this schema?'); 28 | } 29 | 30 | var context = validationContext(schema, { 31 | instance: instance, 32 | refs: this._refs, 33 | breakOnError: options && options.breakOnError, 34 | cleanWithDefaults: options && options.cleanWithDefaults 35 | }); 36 | validators.base(context, instance, schema); 37 | if(context.result.valid) context.result.cleanInstance = context.cleanSubject; 38 | return context.result; 39 | } 40 | }; 41 | 42 | function makeValidator() { 43 | return Object.create(protoValidator, { 44 | _refs: { enumerable:false, writable:false, value:jsonRefs() } 45 | }); 46 | } 47 | 48 | module.exports = makeValidator; 49 | 50 | module.exports.validate = function(instance, schema, options) { 51 | if(instance === undefined) throw new Error('Instance undefined in call to validate.'); 52 | if(!schema) throw new Error('No schema specified in call to validate.'); 53 | 54 | var context = validationContext(schema, { 55 | instance: instance, 56 | breakOnError: options && options.breakOnError, 57 | cleanWithDefaults: options && options.cleanWithDefaults 58 | }); 59 | validators.base(context, instance, schema); 60 | if(context.result.valid) context.result.cleanInstance = context.cleanSubject; 61 | return context.result; 62 | }; 63 | 64 | module.exports.use = function(plugin) { 65 | if(typeof plugin !== 'function') throw new Error('skeemas.use called with non-function. Plugins are in the form function(skeemas){}.'); 66 | plugin(protoValidator); 67 | return this; 68 | }; 69 | -------------------------------------------------------------------------------- /tests/validators/number.test.js: -------------------------------------------------------------------------------- 1 | var validateNumber = require('../../validators').types.number, 2 | validationContext = require('../../validation-context'); 3 | 4 | 5 | describe('Number validator', function() { 6 | describe('for type:number', function() { 7 | it('should validate an integer', function() { 8 | var ctx = validationContext({ type:'number' }); 9 | assert.isTrue(validateNumber(ctx, 42, ctx.schema)); 10 | }); 11 | 12 | it('should validate a non-integer number', function() { 13 | var ctx = validationContext({ type:'number' }); 14 | assert.isTrue(validateNumber(ctx, 42.1337, ctx.schema)); 15 | }); 16 | 17 | it('should validate a number over minimum', function() { 18 | var ctx = validationContext({ type:'number', minimum:0 }); 19 | assert.isTrue(validateNumber(ctx, 42.1337, ctx.schema)); 20 | }); 21 | 22 | it('should invalidate a number under minimum', function() { 23 | var ctx = validationContext({ type:'number', minimum:1337 }); 24 | assert.isFalse(validateNumber(ctx, 42.1337, ctx.schema)); 25 | }); 26 | 27 | it('should validate a number under maximum', function() { 28 | var ctx = validationContext({ type:'number', maximum:1337 }); 29 | assert.isTrue(validateNumber(ctx, 42.1337, ctx.schema)); 30 | }); 31 | 32 | it('should invalidate a number over maximum', function() { 33 | var ctx = validationContext({ type:'number', maximum:0 }); 34 | assert.isFalse(validateNumber(ctx, 42.1337, ctx.schema)); 35 | }); 36 | 37 | it('should validate that 4.5 is divisible by 1.5', function() { 38 | var ctx = validationContext({ divisibleBy:1.5 }); 39 | assert.isTrue(validateNumber(ctx, 4.5, ctx.schema)); 40 | }); 41 | 42 | it('should validate that 0.0075 is multiple of 0.0001', function() { 43 | var ctx = validationContext({ multipleOf:0.0001 }); 44 | assert.isTrue(validateNumber(ctx, 0.0075, ctx.schema)); 45 | }); 46 | }); 47 | 48 | describe('for type:integer', function() { 49 | it('should validate an integer', function() { 50 | var ctx = validationContext({ type:'integer' }); 51 | assert.isTrue(validateNumber(ctx, 42, ctx.schema)); 52 | }); 53 | 54 | it('should validate a non-integer number', function() { 55 | var ctx = validationContext({ type:'integer' }); 56 | assert.isFalse(validateNumber(ctx, 42.1337, ctx.schema)); 57 | }); 58 | 59 | it('should validate an integer over minimum', function() { 60 | var ctx = validationContext({ type:'integer', minimum:0 }); 61 | assert.isTrue(validateNumber(ctx, 42, ctx.schema)); 62 | }); 63 | 64 | it('should invalidate an integer under minimum', function() { 65 | var ctx = validationContext({ type:'integer', minimum:1337 }); 66 | assert.isFalse(validateNumber(ctx, 42, ctx.schema)); 67 | }); 68 | 69 | it('should validate an integer under maximum', function() { 70 | var ctx = validationContext({ type:'integer', maximum:1337 }); 71 | assert.isTrue(validateNumber(ctx, 42, ctx.schema)); 72 | }); 73 | 74 | it('should invalidate an integer over maximum', function() { 75 | var ctx = validationContext({ type:'integer', maximum:0 }); 76 | assert.isFalse(validateNumber(ctx, 42, ctx.schema)); 77 | }); 78 | 79 | it('should validate that 42 is divisible by 6', function() { 80 | var ctx = validationContext({ divisibleBy:6 }); 81 | assert.isTrue(validateNumber(ctx, 42, ctx.schema)); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /validators/array.js: -------------------------------------------------------------------------------- 1 | var validateBase = require('./base'), 2 | deepEqual = require('./deep-equal'); 3 | 4 | function items(context, subject, schema, cleanItems) { 5 | var valid = true; 6 | 7 | if(Array.isArray(schema.items)) { 8 | valid = tupleItems(context, subject, schema, cleanItems); 9 | if('additionalItems' in schema) valid = additionalItems(context, subject, schema, cleanItems) && valid; 10 | } else if(schema.items) { 11 | valid = itemSchema(context, subject, schema, cleanItems); 12 | } 13 | 14 | return valid; 15 | } 16 | 17 | function itemSchema(context, subject, schema, cleanItems) { 18 | var items = schema.items; 19 | 20 | if(typeof items !== 'object') 21 | throw new Error('Invalid schema: invalid "items"'); 22 | 23 | var lastPath = context.path.length; 24 | for(var i = 0, len = subject.length; i < len; i++) { 25 | context.path[lastPath] = i; 26 | if(!validateBase(context, subject[i], items)) { 27 | context.addError('Failed "items" criteria', subject, items); 28 | context.path.length = lastPath; 29 | return false; 30 | } 31 | cleanItems.push(context.cleanSubject); 32 | } 33 | context.path.length = lastPath; 34 | 35 | return true; 36 | } 37 | 38 | function tupleItems(context, subject, schema, cleanItems) { 39 | var items = schema.items, 40 | lastPath = context.path.length; 41 | for(var i = 0, len = items.length, lenSubject = subject.length; i < len && i < lenSubject; i++) { 42 | context.path[lastPath] = i; 43 | if(!validateBase(context, subject[i], items[i])) { 44 | context.addError('Failed "items" criteria', subject, items); 45 | context.path.length = lastPath; 46 | return false; 47 | } 48 | cleanItems.push(context.cleanSubject); 49 | } 50 | context.path.length = lastPath; 51 | 52 | return true; 53 | } 54 | 55 | function additionalItems(context, subject, schema, cleanItems) { 56 | var i = schema.items.length, 57 | len = subject.length, 58 | additionalItemSchema = schema.additionalItems; 59 | 60 | if(additionalItemSchema === false) { 61 | if(len <= i) return true; 62 | 63 | context.addError('Failed "additionalItems" criteria: no additional items are allowed', subject, schema); 64 | return false; 65 | } 66 | 67 | if(typeof additionalItemSchema !== 'object') 68 | throw new Error('Invalid schema: invalid "additionalItems"'); 69 | 70 | var lastPath = context.path.length; 71 | for(; i < len; i++) { 72 | context.path[lastPath] = i; 73 | if(!validateBase(context, subject[i], additionalItemSchema)) { 74 | context.addError('Failed "additionalItems" criteria', subject, schema); 75 | context.path.length = lastPath; 76 | return false; 77 | } 78 | cleanItems.push(context.cleanSubject); 79 | } 80 | context.path.length = lastPath; 81 | 82 | return true; 83 | } 84 | 85 | function minItems(context, subject, schema) { 86 | if(subject.length < schema.minItems) { 87 | context.addError('Failed "minItems" criteria', subject, schema); 88 | return false; 89 | } 90 | 91 | return true; 92 | } 93 | 94 | function maxItems(context, subject, schema) { 95 | if(subject.length > schema.maxItems) { 96 | context.addError('Failed "maxItems" criteria', subject, schema); 97 | return false; 98 | } 99 | 100 | return true; 101 | } 102 | 103 | function uniqueItems(context, subject, schema) { 104 | var i = subject.length, j; 105 | 106 | while(i--) { 107 | j = i; 108 | while(j--) { 109 | if(deepEqual(subject[i], subject[j])) { 110 | context.addError('Failed "uniqueItems" criteria', subject, schema); 111 | return false; 112 | } 113 | } 114 | } 115 | 116 | return true; 117 | } 118 | 119 | 120 | module.exports = function(context, subject, schema) { 121 | if(!Array.isArray(subject)) { 122 | context.addError('Failed type:array criteria', schema); 123 | return false; 124 | } 125 | 126 | var cleanItems = [], 127 | valid = context.runValidations([ 128 | [ 'minItems' in schema, minItems ], 129 | [ 'maxItems' in schema, maxItems ], 130 | [ 'uniqueItems' in schema, uniqueItems ], 131 | [ 'items' in schema, items ] 132 | ], subject, schema, cleanItems); 133 | 134 | if('items' in schema) 135 | context.cleanSubject = cleanItems; 136 | else 137 | context.cleanSubject = subject.slice(); 138 | 139 | return valid; 140 | }; 141 | -------------------------------------------------------------------------------- /tests/validators/base.test.js: -------------------------------------------------------------------------------- 1 | var validateBase = require('../../validators').base, 2 | validationContext = require('../../validation-context'); 3 | 4 | 5 | describe('Base validator', function() { 6 | describe('an empty schema', function() { 7 | it('should validate an array', function() { 8 | var ctx = validationContext({}); 9 | assert.isTrue(validateBase(ctx, [], ctx.schema)); 10 | }); 11 | 12 | it('should validate a boolean true value', function() { 13 | var ctx = validationContext({}); 14 | assert.isTrue(validateBase(ctx, true, ctx.schema)); 15 | }); 16 | 17 | it('should validate a boolean false value', function() { 18 | var ctx = validationContext({}); 19 | assert.isTrue(validateBase(ctx, false, ctx.schema)); 20 | }); 21 | 22 | it('should validate an integer', function() { 23 | var ctx = validationContext({}); 24 | assert.isTrue(validateBase(ctx, 42, ctx.schema)); 25 | }); 26 | 27 | it('should validate a null', function() { 28 | var ctx = validationContext({}); 29 | assert.isTrue(validateBase(ctx, null, ctx.schema)); 30 | }); 31 | 32 | it('should validate a number', function() { 33 | var ctx = validationContext({}); 34 | assert.isTrue(validateBase(ctx, 42.1337, ctx.schema)); 35 | }); 36 | 37 | it('should validate an object', function() { 38 | var ctx = validationContext({}); 39 | assert.isTrue(validateBase(ctx, { test:true }, ctx.schema)); 40 | }); 41 | 42 | it('should validate an empty object', function() { 43 | var ctx = validationContext({}); 44 | assert.isTrue(validateBase(ctx, {}, ctx.schema)); 45 | }); 46 | 47 | it('should validate a string', function() { 48 | var ctx = validationContext({}); 49 | assert.isTrue(validateBase(ctx, 'test', ctx.schema)); 50 | }); 51 | 52 | it('should validate an empty string', function() { 53 | var ctx = validationContext({}); 54 | assert.isTrue(validateBase(ctx, '', ctx.schema)); 55 | }); 56 | }); 57 | 58 | describe('format', function() { 59 | it('should validate integer string utc-millisec', function() { 60 | var ctx = validationContext({ format:'utc-millisec' }); 61 | assert.isTrue(validateBase(ctx, '1337', ctx.schema)); 62 | }); 63 | 64 | it('should invalidate float string utc-millisec', function() { 65 | var ctx = validationContext({ format:'utc-millisec' }); 66 | assert.isFalse(validateBase(ctx, '1337.42', ctx.schema)); 67 | }); 68 | 69 | it('should validate integer number utc-millisec', function() { 70 | var ctx = validationContext({ format:'utc-millisec' }); 71 | assert.isTrue(validateBase(ctx, 1337, ctx.schema)); 72 | }); 73 | 74 | it('should invalidate float number utc-millisec', function() { 75 | var ctx = validationContext({ format:'utc-millisec' }); 76 | assert.isFalse(validateBase(ctx, 1337.42, ctx.schema)); 77 | }); 78 | 79 | it('should validate an email string', function() { 80 | var ctx = validationContext({ format:'email' }); 81 | assert.isTrue(validateBase(ctx, 'foo@bar.com', ctx.schema)); 82 | }); 83 | 84 | it('should validate an uncommon email string', function() { 85 | var ctx = validationContext({ format:'email' }); 86 | assert.isTrue(validateBase(ctx, 'foo.bar+junk@boo.far.museum', ctx.schema)); 87 | }); 88 | }); 89 | 90 | describe('result object', function() { 91 | it('should have valid:true when valid', function() { 92 | var ctx = validationContext({}); 93 | validateBase(ctx, 'test', ctx.schema); 94 | assert.isTrue(ctx.result.valid); 95 | }); 96 | 97 | it('should have valid:false when invalid', function() { 98 | var ctx = validationContext({ type:'object' }); 99 | validateBase(ctx, 'test', ctx.schema); 100 | assert.isFalse(ctx.result.valid); 101 | }); 102 | 103 | it('should have an empty errors array when valid', function() { 104 | var ctx = validationContext({}); 105 | validateBase(ctx, 'test', ctx.schema); 106 | assert.isArray(ctx.result.errors); 107 | assert.lengthOf(ctx.result.errors, 0); 108 | }); 109 | 110 | it('should have populated errors array when invalid', function() { 111 | var ctx = validationContext({ type:'object' }); 112 | validateBase(ctx, 'test', ctx.schema); 113 | assert.isArray(ctx.result.errors); 114 | assert.lengthOf(ctx.result.errors, 1); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /tests/refs/json-schema-draft-03.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-03/schema#", 3 | "id": "http://json-schema.org/draft-03/schema#", 4 | "type": "object", 5 | 6 | "properties": { 7 | "type": { 8 | "type": [ "string", "array" ], 9 | "items": { 10 | "type": [ "string", { "$ref": "#" } ] 11 | }, 12 | "uniqueItems": true, 13 | "default": "any" 14 | }, 15 | 16 | "properties": { 17 | "type": "object", 18 | "additionalProperties": { "$ref": "#" }, 19 | "default": {} 20 | }, 21 | 22 | "patternProperties": { 23 | "type": "object", 24 | "additionalProperties": { "$ref": "#" }, 25 | "default": {} 26 | }, 27 | 28 | "additionalProperties": { 29 | "type": [ { "$ref": "#" }, "boolean" ], 30 | "default": {} 31 | }, 32 | 33 | "items": { 34 | "type": [ { "$ref": "#" }, "array" ], 35 | "items": { "$ref": "#" }, 36 | "default": {} 37 | }, 38 | 39 | "additionalItems": { 40 | "type": [ { "$ref": "#" }, "boolean" ], 41 | "default": {} 42 | }, 43 | 44 | "required": { 45 | "type": "boolean", 46 | "default": false 47 | }, 48 | 49 | "dependencies": { 50 | "type": "object", 51 | "additionalProperties": { 52 | "type": [ "string", "array", { "$ref": "#" } ], 53 | "items": { 54 | "type": "string" 55 | } 56 | }, 57 | "default": {} 58 | }, 59 | 60 | "minimum": { 61 | "type": "number" 62 | }, 63 | 64 | "maximum": { 65 | "type": "number" 66 | }, 67 | 68 | "exclusiveMinimum": { 69 | "type": "boolean", 70 | "default": false 71 | }, 72 | 73 | "exclusiveMaximum": { 74 | "type": "boolean", 75 | "default": false 76 | }, 77 | 78 | "minItems": { 79 | "type": "integer", 80 | "minimum": 0, 81 | "default": 0 82 | }, 83 | 84 | "maxItems": { 85 | "type": "integer", 86 | "minimum": 0 87 | }, 88 | 89 | "uniqueItems": { 90 | "type": "boolean", 91 | "default": false 92 | }, 93 | 94 | "pattern": { 95 | "type": "string", 96 | "format": "regex" 97 | }, 98 | 99 | "minLength": { 100 | "type": "integer", 101 | "minimum": 0, 102 | "default": 0 103 | }, 104 | 105 | "maxLength": { 106 | "type": "integer" 107 | }, 108 | 109 | "enum": { 110 | "type": "array", 111 | "minItems": 1, 112 | "uniqueItems": true 113 | }, 114 | 115 | "default": { 116 | "type": "any" 117 | }, 118 | 119 | "title": { 120 | "type": "string" 121 | }, 122 | 123 | "description": { 124 | "type": "string" 125 | }, 126 | 127 | "format": { 128 | "type": "string" 129 | }, 130 | 131 | "divisibleBy": { 132 | "type": "number", 133 | "minimum": 0, 134 | "exclusiveMinimum": true, 135 | "default": 1 136 | }, 137 | 138 | "disallow": { 139 | "type": [ "string", "array" ], 140 | "items": { 141 | "type": [ "string", { "$ref": "#" } ] 142 | }, 143 | "uniqueItems": true 144 | }, 145 | 146 | "extends": { 147 | "type": [ { "$ref": "#" }, "array" ], 148 | "items": { "$ref": "#" }, 149 | "default": {} 150 | }, 151 | 152 | "id": { 153 | "type": "string", 154 | "format": "uri" 155 | }, 156 | 157 | "$ref": { 158 | "type": "string", 159 | "format": "uri" 160 | }, 161 | 162 | "$schema": { 163 | "type": "string", 164 | "format": "uri" 165 | } 166 | }, 167 | 168 | "dependencies": { 169 | "exclusiveMinimum": "minimum", 170 | "exclusiveMaximum": "maximum" 171 | }, 172 | 173 | "default": {} 174 | } 175 | -------------------------------------------------------------------------------- /tests/refs/json-schema-draft-04.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "$schema": { 35 | "type": "string", 36 | "format": "uri" 37 | }, 38 | "title": { 39 | "type": "string" 40 | }, 41 | "description": { 42 | "type": "string" 43 | }, 44 | "default": {}, 45 | "multipleOf": { 46 | "type": "number", 47 | "minimum": 0, 48 | "exclusiveMinimum": true 49 | }, 50 | "maximum": { 51 | "type": "number" 52 | }, 53 | "exclusiveMaximum": { 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "minimum": { 58 | "type": "number" 59 | }, 60 | "exclusiveMinimum": { 61 | "type": "boolean", 62 | "default": false 63 | }, 64 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 65 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 66 | "pattern": { 67 | "type": "string", 68 | "format": "regex" 69 | }, 70 | "additionalItems": { 71 | "anyOf": [ 72 | { "type": "boolean" }, 73 | { "$ref": "#" } 74 | ], 75 | "default": {} 76 | }, 77 | "items": { 78 | "anyOf": [ 79 | { "$ref": "#" }, 80 | { "$ref": "#/definitions/schemaArray" } 81 | ], 82 | "default": {} 83 | }, 84 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 85 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 86 | "uniqueItems": { 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 91 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "required": { "$ref": "#/definitions/stringArray" }, 93 | "additionalProperties": { 94 | "anyOf": [ 95 | { "type": "boolean" }, 96 | { "$ref": "#" } 97 | ], 98 | "default": {} 99 | }, 100 | "definitions": { 101 | "type": "object", 102 | "additionalProperties": { "$ref": "#" }, 103 | "default": {} 104 | }, 105 | "properties": { 106 | "type": "object", 107 | "additionalProperties": { "$ref": "#" }, 108 | "default": {} 109 | }, 110 | "patternProperties": { 111 | "type": "object", 112 | "additionalProperties": { "$ref": "#" }, 113 | "default": {} 114 | }, 115 | "dependencies": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "anyOf": [ 119 | { "$ref": "#" }, 120 | { "$ref": "#/definitions/stringArray" } 121 | ] 122 | } 123 | }, 124 | "enum": { 125 | "type": "array", 126 | "minItems": 1, 127 | "uniqueItems": true 128 | }, 129 | "type": { 130 | "anyOf": [ 131 | { "$ref": "#/definitions/simpleTypes" }, 132 | { 133 | "type": "array", 134 | "items": { "$ref": "#/definitions/simpleTypes" }, 135 | "minItems": 1, 136 | "uniqueItems": true 137 | } 138 | ] 139 | }, 140 | "allOf": { "$ref": "#/definitions/schemaArray" }, 141 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 142 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 143 | "not": { "$ref": "#" } 144 | }, 145 | "dependencies": { 146 | "exclusiveMaximum": [ "maximum" ], 147 | "exclusiveMinimum": [ "minimum" ] 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /validators/object.js: -------------------------------------------------------------------------------- 1 | var validateBase = require('./base'); 2 | 3 | function properties(context, subject, schema, handledProps) { 4 | var props = schema.properties, 5 | valid = true; 6 | for(var key in props) { 7 | if(key in subject) { 8 | context.path.push(key); 9 | valid = validateBase(context, subject[key], props[key]) && valid; 10 | context.path.pop(); 11 | handledProps[key] = context.cleanSubject; 12 | } else if(props[key].required === true) { 13 | context.addError('Failed "required" criteria: missing property (' + key + ')', subject, props); 14 | valid = false; 15 | } 16 | } 17 | 18 | return valid; 19 | } 20 | 21 | function patternProperties(context, subject, schema, handledProps) { 22 | var patternProps = schema.patternProperties; 23 | 24 | if(typeof patternProps !== 'object') 25 | throw new Error('Invalid schema: "patternProperties" must be an object'); 26 | 27 | var valid = true, 28 | patterns = Object.keys(patternProps), 29 | len = patterns.length, 30 | keys = Object.keys(subject), 31 | i = keys.length, 32 | j, key; 33 | 34 | while(i--) { 35 | key = keys[i]; 36 | 37 | j = len; 38 | while(j--) { 39 | if(key.match(patterns[j])) { 40 | context.path.push(key); 41 | valid = validateBase(context, subject[key], patternProps[patterns[j]]) && valid; 42 | context.path.pop(); 43 | if(!(key in handledProps)) handledProps[key] = context.cleanSubject; 44 | } 45 | } 46 | } 47 | 48 | return valid; 49 | } 50 | 51 | function additionalProperties(context, subject, schema, handledProps) { 52 | var additionalProps = schema.additionalProperties; 53 | 54 | if(additionalProps === true) return true; 55 | 56 | var keys = Object.keys(subject), 57 | i = keys.length; 58 | if(additionalProps === false) { 59 | while(i--) { 60 | if(!(keys[i] in handledProps)) { 61 | context.addError('Failed "additionalProperties" criteria: unexpected property (' + keys[i] + ')', subject, schema); 62 | return false; 63 | } 64 | } 65 | return true; 66 | } 67 | 68 | if(typeof additionalProps !== 'object') 69 | throw new Error('Invalid schema: "additionalProperties" must be a valid schema'); 70 | 71 | var valid; 72 | while(i--) { 73 | if(keys[i] in handledProps) continue; 74 | 75 | context.path.push(keys[i]); 76 | valid = validateBase(context, subject[keys[i]], additionalProps) && valid; 77 | context.path.pop(); 78 | handledProps[keys[i]] = context.cleanSubject; 79 | } 80 | 81 | return valid; 82 | } 83 | 84 | function minProperties(context, subject, schema) { 85 | var keys = Object.keys(subject); 86 | if(keys.length < schema.minProperties) { 87 | context.addError('Failed "minProperties" criteria', subject, schema); 88 | return false; 89 | } 90 | return true; 91 | } 92 | 93 | function maxProperties(context, subject, schema) { 94 | var keys = Object.keys(subject); 95 | if(keys.length > schema.maxProperties) { 96 | context.addError('Failed "maxProperties" criteria', subject, schema); 97 | return false; 98 | } 99 | return true; 100 | } 101 | 102 | function required(context, subject, schema) { 103 | var requiredProps = schema.required; 104 | 105 | if(!Array.isArray(requiredProps)) 106 | throw new Error('Invalid schema: "required" must be an array'); 107 | 108 | var valid = true, 109 | i = requiredProps.length; 110 | while(i--) { 111 | if(!(requiredProps[i] in subject)) { 112 | context.addError('Missing required property "' + requiredProps[i] + '"', subject, requiredProps[i]); 113 | valid = false; 114 | } 115 | } 116 | 117 | return valid; 118 | } 119 | 120 | function dependencies(context, subject, schema) { 121 | var deps = schema.dependencies; 122 | 123 | if(typeof deps !== 'object') 124 | throw new Error('Invalid schema: "dependencies" must be an object'); 125 | 126 | var valid = true, 127 | keys = Object.keys(deps), 128 | i = keys.length, 129 | requiredProps, j; 130 | 131 | while(i--) { 132 | if(!(keys[i] in subject)) continue; 133 | 134 | requiredProps = deps[keys[i]]; 135 | 136 | if(typeof requiredProps === 'string') requiredProps = [ requiredProps ]; 137 | 138 | if(Array.isArray(requiredProps)) { 139 | j = requiredProps.length; 140 | while(j--) { 141 | if(!(requiredProps[j] in subject)) { 142 | context.addError('Missing required property "' + requiredProps[j] + '"', subject, requiredProps[j]); 143 | valid = false; 144 | } 145 | } 146 | } else if(typeof requiredProps === 'object') { 147 | valid = validateBase(context, subject, requiredProps) && valid; 148 | } else { 149 | throw new Error('Invalid schema: dependencies must be string, array, or object'); 150 | } 151 | } 152 | 153 | return valid; 154 | } 155 | 156 | function addDefaults(subject, schema) { 157 | var props = schema.properties; 158 | 159 | if(!props) return; 160 | 161 | for(var key in props) { 162 | if('default' in props[key] && !(key in subject)) { 163 | subject[key] = props[key].default; 164 | } 165 | } 166 | } 167 | 168 | 169 | function validateObject(context, subject, schema) { 170 | if(typeof subject !== 'object') { 171 | context.addError('Failed type:object criteria', subject, schema); 172 | return false; 173 | } 174 | 175 | var handledProps = {}, 176 | valid = context.runValidations([ 177 | [ 'properties' in schema, properties ], 178 | [ 'patternProperties' in schema, patternProperties ], 179 | [ 'additionalProperties' in schema, additionalProperties ], 180 | [ 'minProperties' in schema, minProperties ], 181 | [ 'maxProperties' in schema, maxProperties ], 182 | [ Array.isArray(schema.required), required ], 183 | [ 'dependencies' in schema, dependencies ] 184 | ], subject, schema, handledProps); 185 | 186 | if(context.cleanWithDefaults) addDefaults(handledProps, schema); 187 | 188 | context.cleanSubject = handledProps; 189 | 190 | return valid; 191 | } 192 | 193 | module.exports = validateObject; 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # skeemas 2 | Comprehensive JSON Schema (drafts 3 and 4) validation. 3 | 4 | 5 | ## Installation 6 | ```bash 7 | npm install skeemas --save 8 | ``` 9 | 10 | 11 | ## Basic Validation 12 | **`skeemas.validate(subject, schema[, options])`** 13 | 14 | ```js 15 | var skeemas = require('skeemas'); 16 | 17 | skeemas.validate('foo', { type:'string' }).valid; // true 18 | skeemas.validate(10000, { type:'string' }).valid; // false 19 | skeemas.validate(10000, { type:'number' }).valid; // true 20 | 21 | // Result contains an array of errors 22 | var result = skeemas.validate('test', { enum:['foobar'], minLength:5 }); 23 | result.valid; // false 24 | result.errors; // array with 2 error objects 25 | 26 | // Pass the "breakOnError" option to stop processing on the first error 27 | var result = skeemas.validate('test', { enum:['foobar'], minLength:5 }, { breakOnError:true }); 28 | result.valid; // false 29 | result.errors; // array with 1 error object 30 | 31 | var result = skeemas.validate({ 32 | foo: 'bar', 33 | nested: { 34 | stuff: [1,2,3], 35 | ignoreMe: 'undeclared property' 36 | } 37 | }, { 38 | properties: { 39 | foo: { type:'string' }, 40 | nested: { 41 | properties: { 42 | stuff: { 43 | type: 'array', 44 | items: { type:'integer' } 45 | } 46 | // We aren't going to declare `ignoreMe`. To disallow extra 47 | // props we could set `additionalProperties:false`. 48 | } 49 | } 50 | } 51 | }); 52 | result.valid; // true 53 | assert.deepEqual(result.cleanInstance, { 54 | foo: 'bar', 55 | nested: { 56 | stuff: [1,2,3] 57 | // notice the `ignoreMe` property is removed from `cleanInstance` 58 | } 59 | }); 60 | ``` 61 | 62 | For more information about constructing schemas see http://json-schema.org/ or the wonderful guide at http://spacetelescope.github.io/understanding-json-schema/index.html 63 | 64 | 65 | ## Adding Schemas 66 | Skeemas supports validation by schema id and refrences between schemas via the `$ref` property: 67 | 68 | ```js 69 | // Create an instance of a validator 70 | var validator = require('skeemas')(); 71 | 72 | // Add schemas to the validator 73 | validator.addRef({ type:'string', pattern:'^[a-z0-9]+$' }, '/identifier'); 74 | 75 | // Validate by uri/id 76 | validator.validate('foo123', '/identifier').valid; // true 77 | 78 | // Use a $ref reference in other schemas 79 | validator.validate(user, { 80 | type: 'object', 81 | properties: { 82 | id: { '$ref':'/identifier' }, 83 | name: { type:'string' } 84 | } 85 | }).valid; // true 86 | ``` 87 | 88 | 89 | ## Related Modules 90 | 91 | - [skeemas-body-parser](https://github.com/Prestaul/skeemas-body-parser) - json body parser middleware with schema validation 92 | - [skeemas-markdown-validation](https://github.com/Prestaul/skeemas-markdown-validation) - simple testing of json blocks in your markdown documentation 93 | 94 | 95 | ## Development 96 | Our tests are running the JSON Schema test suite at [https://github.com/json-schema/JSON-Schema-Test-Suite](https://github.com/json-schema/JSON-Schema-Test-Suite). Those tests are referenced as a submodule and therefore dev setup is a little non-standard. 97 | ```bash 98 | # clone the repo 99 | 100 | # install dependencies from npm 101 | npm install 102 | 103 | # install the test suite 104 | git submodule init 105 | git submodule update 106 | 107 | # run the tests 108 | npm test 109 | ``` 110 | 111 | 112 | 113 | ## Feature Status 114 | 115 | - [X] Full Validation (all errors) 116 | - [X] Quick Validation (first error) 117 | - [X] Instance cleaning 118 | - [X] Manual reference additions 119 | - [X] Validate by reference 120 | - [ ] Missing reference resolution 121 | - [ ] Custom format validation 122 | - [ ] Custom attribute validation 123 | - [X] Plugins 124 | - [X] JSON-Schema draft 03 and 04 feature support 125 | - Ignored schema attributes 126 | - $schema 127 | - title 128 | - description 129 | - default 130 | - [X] References 131 | - [X] id 132 | - [X] definitions 133 | - [X] $ref 134 | - [X] Validations by type 135 | - [X] any 136 | - [X] type 137 | - [X] enum 138 | - [X] extends 139 | - [X] allOf 140 | - [X] anyOf 141 | - [X] oneOf 142 | - [X] not 143 | - [X] disallow 144 | - [X] required 145 | - [X] format 146 | - [X] array 147 | - [X] items 148 | - [X] additionalItems 149 | - [X] minItems 150 | - [X] maxItems 151 | - [X] uniqueItems 152 | - [X] boolean 153 | - [X] null 154 | - [X] number, integer 155 | - [X] multipleOf 156 | - [X] divisibleBy 157 | - [X] minimum 158 | - [X] maximum 159 | - [X] exclusiveMinimum 160 | - [X] exclusiveMaximum 161 | - [X] object 162 | - [X] properties 163 | - [X] patternProperties 164 | - [X] additionalProperties 165 | - [X] required 166 | - [X] dependencies 167 | - [X] minProperties 168 | - [X] maxProperties 169 | - [X] dependencies 170 | - [X] string 171 | - [X] minLength 172 | - [X] maxLength 173 | - [X] pattern 174 | - [X] format 175 | - [X] date-time 176 | - [X] date 177 | - [X] time 178 | - [X] utc-millisec 179 | - [X] email 180 | - [X] hostname 181 | - [X] host-name 182 | - [X] ip-address 183 | - [X] ipv4 184 | - [X] ipv6 185 | - [X] uri 186 | - [X] regex 187 | - [X] color 188 | - [X] style 189 | - [X] phone 190 | -------------------------------------------------------------------------------- /validators/base.js: -------------------------------------------------------------------------------- 1 | var INVALID_ESCAPES = /[^\\]\\[^.*+?^${}()|[\]\\bBcdDfnrsStvwWxu0-9]/; 2 | var validators = require('./'), 3 | formats = { 4 | 'date-time': /^\d{4}-(0[0-9]{1}|1[0-2]{1})-[0-9]{2}[t ]\d{2}:\d{2}:\d{2}(\.\d+)?([zZ]|[+-]\d{2}:\d{2})$/i, 5 | 'date': /^\d{4}-(0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/, 6 | 'time': /^\d{2}:\d{2}:\d{2}$/, 7 | 'color': /^(#[0-9a-f]{3}|#[0-9a-f]{6}|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)$/i, 8 | 'style': /^(?:\s*-?[_A-Z]+[_A-Z0-9-]*:[^\n\r\f;]+;)*\s*-?[_A-Z]+[_A-Z0-9-]*:[^\n\r\f;]+;?\s*$/i, 9 | 'phone': /^(?:(?:\(?(?:00|\+)(?:[1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?)?(?:(?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(?:\d+))?$/i, 10 | 'uri': /^(?:([a-z0-9+.-]+:\/\/)((?:(?:[a-z0-9-._~!$&'()*+,;=:]|%[0-9A-F]{2})*)@)?((?:[a-z0-9-._~!$&'()*+,;=]|%[0-9A-F]{2})*)(:(?:\d*))?(\/(?:[a-z0-9-._~!$&'()*+,;=:@\/]|%[0-9A-F]{2})*)?|([a-z0-9+.-]+:)(\/?(?:[a-z0-9-._~!$&'()*+,;=:@]|%[0-9A-F]{2})(?:[a-z0-9-._~!$&'()*+,;=:@\/]|%[0-9A-F]{2})*)?)(\?(?:[a-z0-9-._~!$&'()*+,;=:\/?@]|%[0-9A-F]{2})*)?(#(?:[a-z0-9-._~!$&'()*+,;=:\/?@]|%[0-9A-F]{2})*)?$/i, 11 | 'email': /^[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,}$/i, 12 | 'ipv4': /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/, 13 | 'ipv6': /^\s*((([0-9A-F]{1,4}:){7}([0-9A-F]{1,4}|:))|(([0-9A-F]{1,4}:){6}(:[0-9A-F]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-F]{1,4}:){5}(((:[0-9A-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-F]{1,4}:){4}(((:[0-9A-F]{1,4}){1,3})|((:[0-9A-F]{1,4})((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){3}(((:[0-9A-F]{1,4}){1,4})|((:[0-9A-F]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){2}(((:[0-9A-F]{1,4}){1,5})|((:[0-9A-F]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){1}(((:[0-9A-F]{1,4}){1,6})|((:[0-9A-F]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-F]{1,4}){1,7})|((:[0-9A-F]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/i, 14 | 15 | // hostname regex from: http://stackoverflow.com/a/1420225/5628 16 | 'hostname': /^(?=.{1,255}$)[0-9A-Z](?:(?:[0-9A-Z]|-){0,61}[0-9A-Z])?(?:\.[0-9A-Z](?:(?:[0-9A-Z]|-){0,61}[0-9A-Z])?)*\.?$/i, 17 | 18 | 'utc-millisec': function(subject) { 19 | var parsed = parseInt(subject, 10); 20 | return !isNaN(parsed) && parsed.toString() === subject.toString(); 21 | }, 22 | 'regex': function (subject) { 23 | if(INVALID_ESCAPES.test(subject)) return false; 24 | try { 25 | new RegExp(subject); 26 | return true; 27 | } 28 | catch(e) { 29 | return false; 30 | } 31 | } 32 | }; 33 | 34 | // aliases 35 | formats['host-name'] = formats.hostname; 36 | formats['ip-address'] = formats.ipv4; 37 | 38 | 39 | function getType(subject) { 40 | var type = typeof subject; 41 | 42 | if(type === 'object') { 43 | if(subject === null) return 'null'; 44 | if(Array.isArray(subject)) return 'array'; 45 | } 46 | 47 | if(type === 'number' && subject === Math.round(subject)) return 'integer'; 48 | 49 | return type; 50 | } 51 | 52 | function format(context, subject, schema) { 53 | var fmt = schema.format, 54 | validator = formats[fmt]; 55 | 56 | if(!validator) 57 | throw new Error('Invalid schema: unknown format (' + fmt + ')'); 58 | 59 | var valid = validator.test ? validator.test(subject) : validator(subject); 60 | if(!valid) { 61 | context.addError('Failed "format" criteria (' + fmt + ')', subject, schema); 62 | } 63 | 64 | return valid; 65 | } 66 | 67 | function validateTypes(context, subject, type, validTypes) { 68 | var i = validTypes.length, 69 | validType, valid; 70 | while(i--) { 71 | validType = validTypes[i]; 72 | 73 | if(validType === 'any') return true; 74 | 75 | if(typeof validType === 'object') { 76 | valid = context.silently(function() { 77 | return validateBase(context, subject, validType); 78 | }); // jshint ignore:line 79 | if(valid) return true; 80 | else continue; 81 | } 82 | 83 | if(!(validType in validators.types)) 84 | throw new Error('Invalid schema: invalid type (' + validType + ')'); 85 | 86 | if(validType === 'number' && type === 'integer') return true; 87 | 88 | if(type === validType) return true; 89 | } 90 | 91 | return false; 92 | } 93 | 94 | function allOf(context, subject, schema) { 95 | var schemas = schema.allOf; 96 | 97 | if(!Array.isArray(schemas)) 98 | throw new Error('Invalid schema: "allOf" value must be an array'); 99 | 100 | var i = schemas.length, 101 | invalidCount = 0; 102 | while(i--) { 103 | if(!validateBase(context, subject, schemas[i])) { 104 | invalidCount += 1; 105 | } 106 | } 107 | 108 | if(invalidCount === 0) return true; 109 | 110 | context.addError('Failed "allOf" criteria', subject, schemas); 111 | return false; 112 | } 113 | 114 | function anyOf(context, subject, schema) { 115 | var schemas = schema.anyOf; 116 | 117 | if(!Array.isArray(schemas)) 118 | throw new Error('Invalid schema: "anyOf" value must be an array'); 119 | 120 | var matched = context.silently(function() { 121 | var i = schemas.length; 122 | while(i--) { 123 | if(validateBase(context, subject, schemas[i])) return true; 124 | } 125 | return false; 126 | }); 127 | 128 | if(matched) return true; 129 | 130 | context.addError('Failed "anyOf" criteria', subject, schemas); 131 | return false; 132 | } 133 | 134 | function oneOf(context, subject, schema) { 135 | var schemas = schema.oneOf; 136 | 137 | if(!Array.isArray(schemas)) 138 | throw new Error('Invalid schema: "oneOf" value must be an array'); 139 | 140 | var i = schemas.length, 141 | validCount = 0; 142 | context.silently(function() { 143 | while(i--) { 144 | if(validateBase(context, subject, schemas[i])) validCount += 1; 145 | } 146 | }); 147 | 148 | if(validCount === 1) return true; 149 | 150 | context.addError('Failed "oneOf" criteria', subject, schemas); 151 | return false; 152 | } 153 | 154 | function not(context, subject, schema) { 155 | var badSchema = schema.not, 156 | valid = context.silently(function() { 157 | return !validateBase(context, subject, badSchema); 158 | }); 159 | 160 | if(valid) return true; 161 | 162 | context.addError('Failed "not" criteria', subject, schema); 163 | return false; 164 | } 165 | 166 | function disallow(context, subject, schema, type) { 167 | var invalidTypes = Array.isArray(schema.disallow) ? schema.disallow : [ schema.disallow ], 168 | valid = !validateTypes(context, subject, type, invalidTypes); 169 | 170 | if(!valid) { 171 | context.addError('Failed "disallow" criteria: expecting ' + invalidTypes.join(' or ') + ', found ' + type, subject, schema); 172 | } 173 | 174 | return valid; 175 | } 176 | 177 | function validateExtends(context, subject, schema) { 178 | var schemas = Array.isArray(schema["extends"]) ? schema["extends"] : [ schema["extends"] ]; 179 | 180 | var i = schemas.length, 181 | invalidCount = 0; 182 | while(i--) { 183 | if(!validateBase(context, subject, schemas[i])) { 184 | invalidCount += 1; 185 | } 186 | } 187 | 188 | return invalidCount === 0; 189 | } 190 | 191 | function validateEnum(context, subject, schema) { 192 | var values = schema['enum']; 193 | 194 | if(!Array.isArray(values)) 195 | throw new Error('Invalid schema: "enum" value must be an array'); 196 | 197 | var i = values.length; 198 | while(i--) { 199 | if(validators.deepEqual(subject, values[i])) return true; 200 | } 201 | 202 | context.addError('Failed "enum" criteria', subject, values); 203 | return false; 204 | } 205 | 206 | function validateType(context, subject, schema, type) { 207 | var validTypes = Array.isArray(schema.type) ? schema.type : [ schema.type ], 208 | valid = validateTypes(context, subject, type, validTypes); 209 | 210 | if(!valid) { 211 | context.addError('Failed "type" criteria: expecting ' + validTypes.join(' or ') + ', found ' + type, subject, schema); 212 | } 213 | 214 | return valid; 215 | } 216 | 217 | function typeValidations(context, subject, schema, type) { 218 | return validators.types[type](context, subject, schema); 219 | } 220 | 221 | function pathFromIds(ids) { 222 | return ids.map(function(id) { 223 | var lastSlash = id.lastIndexOf("/"); 224 | if(lastSlash === -1) return id; 225 | return id.substr(0, lastSlash + 1); 226 | }).join(''); 227 | } 228 | 229 | function $ref(context, subject, schema) { 230 | var absolute = /^(#|\/)/.test(schema.$ref), 231 | ref = absolute ? schema.$ref : pathFromIds(context.id) + schema.$ref, 232 | refSchema = context.refs.get(ref, context.schema), 233 | ctx = context; 234 | 235 | if(schema.$ref[0] !== '#') { 236 | ctx = context.subcontext(context.refs.get(ref, context.schema, true)); 237 | } 238 | 239 | var valid = validateBase(ctx, subject, refSchema); 240 | 241 | context.cleanSubject = ctx.cleanSubject; 242 | 243 | return valid; 244 | } 245 | 246 | 247 | 248 | function validateBase(context, subject, schema) { 249 | if(schema.$ref) { 250 | return $ref(context, subject, schema); 251 | } 252 | 253 | if(schema.id) context.id.push(schema.id); 254 | 255 | var valid = context.runValidations([ 256 | [ 'type' in schema, validateType ], 257 | [ 'disallow' in schema, disallow ], 258 | [ 'enum' in schema, validateEnum ], 259 | [ true, typeValidations ], 260 | [ 'format' in schema, format ], 261 | [ 'extends' in schema, validateExtends ], 262 | [ 'allOf' in schema, allOf ], 263 | [ 'anyOf' in schema, anyOf ], 264 | [ 'oneOf' in schema, oneOf ], 265 | [ 'not' in schema, not ] 266 | ], subject, schema, getType(subject)); 267 | 268 | if(schema.id) context.id.pop(); 269 | 270 | return valid; 271 | } 272 | 273 | module.exports = validateBase; 274 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | var skeemas = require('../'); 2 | 3 | describe('Validate', function() { 4 | it('should throw an error if missing schema', function() { 5 | assert.throws(function() { 6 | validate('test'); 7 | }); 8 | }); 9 | 10 | it('should throw an error if adding a reference schema without a uri/id', function() { 11 | assert.throws(function() { 12 | skeemas().addRef({}); 13 | }); 14 | }); 15 | 16 | describe('an empty schema', function() { 17 | it('should validate an array', function() { 18 | assert.isTrue(validate([], {}).valid); 19 | }); 20 | 21 | it('should validate a boolean true value', function() { 22 | assert.isTrue(validate(true, {}).valid); 23 | }); 24 | 25 | it('should validate a boolean false value', function() { 26 | assert.isTrue(validate(false, {}).valid); 27 | }); 28 | 29 | it('should validate an integer', function() { 30 | assert.isTrue(validate(42, {}).valid); 31 | }); 32 | 33 | it('should validate a null', function() { 34 | assert.isTrue(validate(null, {}).valid); 35 | }); 36 | 37 | it('should validate a number', function() { 38 | assert.isTrue(validate(42.1337, {}).valid); 39 | }); 40 | 41 | it('should validate an object', function() { 42 | assert.isTrue(validate({ test:true }, {}).valid); 43 | }); 44 | 45 | it('should validate an empty object', function() { 46 | assert.isTrue(validate({}, {}).valid); 47 | }); 48 | 49 | it('should validate a string', function() { 50 | assert.isTrue(validate('test', {}).valid); 51 | }); 52 | 53 | it('should validate an empty string', function() { 54 | assert.isTrue(validate('', {}).valid); 55 | }); 56 | }); 57 | 58 | describe('result object', function() { 59 | it('should have valid:true when valid', function() { 60 | var result = validate('test', {}); 61 | assert.isTrue(result.valid); 62 | }); 63 | 64 | it('should have valid:false when invalid', function() { 65 | var result = validate('test', { type:'object' }); 66 | assert.isFalse(result.valid); 67 | }); 68 | 69 | it('should have an empty errors array when valid', function() { 70 | var result = validate('test', {}); 71 | assert.isArray(result.errors); 72 | assert.lengthOf(result.errors, 0); 73 | }); 74 | 75 | it('should have populated errors array when invalid', function() { 76 | var result = validate('test', { type:'object' }); 77 | assert.isArray(result.errors); 78 | assert.lengthOf(result.errors, 1); 79 | }); 80 | 81 | it('should have multiple errors when multiple conditions are invalid', function() { 82 | var result = validate('test', { enum:['foobar'], minLength:5 }); 83 | assert.isArray(result.errors); 84 | assert.lengthOf(result.errors, 2); 85 | }); 86 | 87 | it('should have single error if breakOnError', function() { 88 | var result = validate('test', { enum:['foobar'], minLength:5 }, { breakOnError:true }); 89 | assert.isArray(result.errors); 90 | assert.lengthOf(result.errors, 1); 91 | }); 92 | }); 93 | 94 | describe('result.cleanInstance', function() { 95 | it('should leave null untouched', function() { 96 | var result = validate(null, { type:'null' }); 97 | assert.strictEqual(result.cleanInstance, null); 98 | }); 99 | 100 | it('should leave true untouched', function() { 101 | var result = validate(true, { type:'boolean' }); 102 | assert.strictEqual(result.cleanInstance, true); 103 | }); 104 | 105 | it('should leave false untouched', function() { 106 | var result = validate(false, { type:'boolean' }); 107 | assert.strictEqual(result.cleanInstance, false); 108 | }); 109 | 110 | it('should leave an integer untouched', function() { 111 | var result = validate(1337, { type:'integer' }); 112 | assert.strictEqual(result.cleanInstance, 1337); 113 | }); 114 | 115 | it('should leave a number untouched', function() { 116 | var result = validate(42.1337, { type:'number' }); 117 | assert.strictEqual(result.cleanInstance, 42.1337); 118 | }); 119 | 120 | it('should leave a string untouched', function() { 121 | var result = validate('test', { type:'string' }); 122 | assert.strictEqual(result.cleanInstance, 'test'); 123 | }); 124 | 125 | it('should clone an array', function() { 126 | var subject = [1, 2, 3], 127 | result = validate(subject, { type:'array' }); 128 | assert.deepEqual(result.cleanInstance, subject); 129 | assert.notStrictEqual(result.cleanInstance, subject); 130 | }); 131 | 132 | describe('an object', function() { 133 | it('should leave defined properties', function() { 134 | var result = validate({ 135 | foo: 'bar', 136 | foobar: 42, 137 | foobat: [1, 'test'], 138 | foobaz: 'far', 139 | boo: 'far', 140 | boofar: 'bad' 141 | }, { 142 | properties: { 143 | foo: {}, 144 | boo: {} 145 | } 146 | }); 147 | 148 | assert.deepEqual(result.cleanInstance, { 149 | foo: 'bar', 150 | boo: 'far' 151 | }); 152 | }); 153 | 154 | it('should leave pattern properties', function() { 155 | var result = validate({ 156 | foo: 'bar', 157 | foobar: 42, 158 | foobat: [1, 'test'], 159 | foobaz: 'far', 160 | boo: 'far', 161 | boofar: 'bad' 162 | }, { 163 | properties: { 164 | boo: {} 165 | }, 166 | patternProperties: { 167 | '^foo': {} 168 | } 169 | }); 170 | 171 | assert.deepEqual(result.cleanInstance, { 172 | foo: 'bar', 173 | foobar: 42, 174 | foobat: [1, 'test'], 175 | foobaz: 'far', 176 | boo: 'far' 177 | }); 178 | }); 179 | 180 | it('should remove undefined additional properties', function() { 181 | var result = validate({ 182 | foo: 'bar', 183 | foobar: 42, 184 | boo: 'far' 185 | }, { 186 | properties: { 187 | foo: {} 188 | }, 189 | additionalProperties: true 190 | }); 191 | 192 | assert.deepEqual(result.cleanInstance, { 193 | foo: 'bar' 194 | }); 195 | }); 196 | 197 | it('should leave defined additional properties', function() { 198 | var result = validate({ 199 | foo: 'bar', 200 | foobar: 42, 201 | boo: 'far' 202 | }, { 203 | properties: { 204 | foo: {} 205 | }, 206 | additionalProperties: { type:'any' } 207 | }); 208 | 209 | assert.deepEqual(result.cleanInstance, { 210 | foo: 'bar', 211 | foobar: 42, 212 | boo: 'far' 213 | }); 214 | }); 215 | 216 | it('should leave defined nested properties', function() { 217 | var result = validate({ 218 | foo: 'bar', 219 | boo: 'far', 220 | nested: { 221 | foo: 'nestbar', 222 | boo: 'nestfar', 223 | arr: [{ 224 | foo:'arrfoo', 225 | boo:'nestbar' 226 | }] 227 | } 228 | }, { 229 | definitions: { 230 | foo: { 231 | properties: { 232 | foo: {}, 233 | arr: { 234 | items: { $ref:'#/definitions/foo' } 235 | }, 236 | nested: { $ref:'#/definitions/foo' } 237 | } 238 | } 239 | }, 240 | $ref:'#/definitions/foo' 241 | }); 242 | 243 | assert.deepEqual(result.cleanInstance, { 244 | foo: 'bar', 245 | nested: { 246 | foo: 'nestbar', 247 | arr: [{ 248 | foo:'arrfoo' 249 | }] 250 | } 251 | }); 252 | }); 253 | 254 | it('should clean instance properly even with reference properties', function() { 255 | var localValidator = skeemas().addRef('/ref', {}), 256 | result = localValidator.validate({ 257 | foo: 'bar', 258 | boo: 'far' 259 | }, { 260 | properties: { 261 | foo: { '$ref':'/ref' }, 262 | boo: {} 263 | } 264 | }); 265 | 266 | assert.deepEqual(result.cleanInstance, { 267 | foo: 'bar', 268 | boo: 'far' 269 | }); 270 | }); 271 | 272 | describe('with cleanWithDefaults', function() { 273 | it('should add defaults', function() { 274 | var result = validate({}, { 275 | properties: { 276 | foo: { default:'bar' }, 277 | boo: { default:'far' } 278 | } 279 | }, { cleanWithDefaults:true }); 280 | 281 | assert.deepEqual(result.cleanInstance, { 282 | foo: 'bar', 283 | boo: 'far' 284 | }); 285 | }); 286 | 287 | it('should not override values with defaults', function() { 288 | var result = validate({ 289 | foo: 'bat' 290 | }, { 291 | properties: { 292 | foo: { default:'bar' }, 293 | boo: { default:'far' } 294 | } 295 | }, { cleanWithDefaults:true }); 296 | 297 | assert.deepEqual(result.cleanInstance, { 298 | foo: 'bat', 299 | boo: 'far' 300 | }); 301 | }); 302 | 303 | it('should add complex default', function() { 304 | var result = validate({}, { 305 | properties: { 306 | foo: { default: { bar:'bat' } } 307 | } 308 | }, { cleanWithDefaults:true }); 309 | 310 | assert.deepEqual(result.cleanInstance, { 311 | foo: { bar:'bat' } 312 | }); 313 | }); 314 | 315 | it('should add null default', function() { 316 | var result = validate({}, { 317 | properties: { 318 | foo: { default: null } 319 | } 320 | }, { cleanWithDefaults:true }); 321 | 322 | assert.deepEqual(result.cleanInstance, { 323 | foo: null 324 | }); 325 | }); 326 | }); 327 | }); 328 | }); 329 | 330 | describe('with reference schema', function() { 331 | var localValidator = skeemas().addRef('/some/schema', { 332 | properties: { foo: { type:'string' } } 333 | }); 334 | 335 | it('should validate against a referenced schema', function() { 336 | assert.isTrue(localValidator.validate({ foo:'test' }, '/some/schema').valid); 337 | }); 338 | 339 | it('should invalidate against a referenced schema', function() { 340 | assert.isFalse(localValidator.validate({ foo:42 }, '/some/schema').valid); 341 | }); 342 | 343 | it('should validate against a referenced schema fragment', function() { 344 | assert.isTrue(localValidator.validate('test', '/some/schema#/properties/foo').valid); 345 | }); 346 | 347 | it('should invalidate against a referenced schema fragment', function() { 348 | assert.isFalse(localValidator.validate(42, '/some/schema#/properties/foo').valid); 349 | }); 350 | 351 | it('should throw an error for missing reference', function() { 352 | assert.throws(function() { 353 | localValidator.validate('test', '/some/missing/schema'); 354 | }); 355 | }); 356 | 357 | it('should throw an error for missing fragment reference', function() { 358 | assert.throws(function() { 359 | localValidator.validate('test', '/some/schema#/not/here'); 360 | }); 361 | }); 362 | }); 363 | 364 | describe('with reference schema added by id', function() { 365 | var localValidator = skeemas().addRef({ 366 | id: '/some/schema', 367 | properties: { foo: { type:'string' } } 368 | }); 369 | 370 | it('should validate against a referenced schema', function() { 371 | assert.isTrue(localValidator.validate({ foo:'test' }, '/some/schema').valid); 372 | }); 373 | 374 | it('should invalidate against a referenced schema', function() { 375 | assert.isFalse(localValidator.validate({ foo:42 }, '/some/schema').valid); 376 | }); 377 | 378 | it('should validate against a referenced schema fragment', function() { 379 | assert.isTrue(localValidator.validate('test', '/some/schema#/properties/foo').valid); 380 | }); 381 | 382 | it('should invalidate against a referenced schema fragment', function() { 383 | assert.isFalse(localValidator.validate(42, '/some/schema#/properties/foo').valid); 384 | }); 385 | 386 | it('should throw an error for missing reference', function() { 387 | assert.throws(function() { 388 | localValidator.validate('test', '/some/missing/schema'); 389 | }); 390 | }); 391 | 392 | it('should throw an error for missing fragment reference', function() { 393 | assert.throws(function() { 394 | localValidator.validate('test', '/some/schema#/not/here'); 395 | }); 396 | }); 397 | }); 398 | 399 | describe('schema having anyOf with references', function() { 400 | var localValidator = skeemas().addRef('/foo/schema', { 401 | enum: [ 'foo' ] 402 | }), 403 | schema = { 404 | definitions: { 405 | bars: { 406 | enum: [ 'bar' ] 407 | }, 408 | ints: { 409 | type: 'integer' 410 | } 411 | }, 412 | anyOf: [ 413 | { $ref: '#/definitions/ints' }, 414 | { $ref: '/foo/schema' }, 415 | { $ref: '#/definitions/bars' } 416 | ] 417 | }; 418 | 419 | it('should validate correctly', function() { 420 | assert.isTrue(localValidator.validate(42, schema).valid, 'should validate an int'); 421 | assert.isTrue(localValidator.validate('foo', schema).valid, 'should validate "foo"'); 422 | assert.isTrue(localValidator.validate('bar', schema).valid, 'should validate "bar"'); 423 | }); 424 | 425 | it('should invalidate correctly', function() { 426 | var result = localValidator.validate(42.1337, schema); 427 | assert.isFalse(result.valid, 'float should be invalid'); 428 | assert.lengthOf(result.errors, 1, 'should have one error'); 429 | assert.match(result.errors[0].message, /anyOf/, 'should be an "anyOf" error'); 430 | 431 | result = localValidator.validate('boo', schema); 432 | assert.isFalse(result.valid, '"boo" should be invalid'); 433 | assert.lengthOf(result.errors, 1, 'should have one error'); 434 | assert.match(result.errors[0].message, /anyOf/, 'should be an "anyOf" error'); 435 | }); 436 | }); 437 | 438 | describe('schema having oneOf with references', function() { 439 | var localValidator = skeemas().addRef('/foo/schema', { 440 | enum: [ 'foo' ] 441 | }), 442 | schema = { 443 | definitions: { 444 | bars: { 445 | enum: [ 'bar' ] 446 | }, 447 | ints: { 448 | type: 'integer' 449 | } 450 | }, 451 | oneOf: [ 452 | { $ref: '#/definitions/ints' }, 453 | { $ref: '/foo/schema' }, 454 | { $ref: '#/definitions/bars' } 455 | ] 456 | }; 457 | 458 | it('should validate correctly', function() { 459 | assert.isTrue(localValidator.validate(42, schema).valid, 'should validate an int'); 460 | assert.isTrue(localValidator.validate('foo', schema).valid, 'should validate "foo"'); 461 | assert.isTrue(localValidator.validate('bar', schema).valid, 'should validate "bar"'); 462 | }); 463 | 464 | it('should invalidate correctly', function() { 465 | var result = localValidator.validate(42.1337, schema); 466 | assert.isFalse(result.valid, 'float should be invalid'); 467 | assert.lengthOf(result.errors, 1, 'should have one error'); 468 | assert.match(result.errors[0].message, /oneOf/, 'should be an "oneOf" error'); 469 | 470 | result = localValidator.validate('boo', schema); 471 | assert.isFalse(result.valid, '"boo" should be invalid'); 472 | assert.lengthOf(result.errors, 1, 'should have one error'); 473 | assert.match(result.errors[0].message, /oneOf/, 'should be an "oneOf" error'); 474 | }); 475 | }); 476 | 477 | it('should handle required:true in props', function() { 478 | var schema = { 479 | properties: { 480 | foo: { 481 | required: true 482 | } 483 | } 484 | }; 485 | assert.isTrue(validate({ foo: { bar:1 } }, schema).valid); 486 | assert.isFalse(validate({ boo: { bar:1 } }, schema).valid); 487 | }); 488 | 489 | it('should handle required:[] in props', function() { 490 | var schema = { 491 | properties: { 492 | foo: { 493 | required: ['bar'] 494 | } 495 | } 496 | }; 497 | assert.isTrue(validate({ foo: { bar:1 } }, schema).valid); 498 | assert.isFalse(validate({ foo: { far:1 } }, schema).valid); 499 | assert.isTrue(validate({ boo: { far:1 } }, schema).valid); 500 | }); 501 | 502 | describe('errors', function() { 503 | describe('after arrays of objects', function() { 504 | it('should have correct context', function() { 505 | var result = validate({ 506 | "someArray": [ {} ] 507 | }, { 508 | properties: { 509 | someArray: { 510 | items: { 511 | type: "object", 512 | properties: { 513 | foo: { required: true } 514 | } 515 | } 516 | }, 517 | missingProp: { 518 | required: true 519 | } 520 | } 521 | }); 522 | 523 | assert.isFalse(result.valid); 524 | assert.lengthOf(result.errors, 3); 525 | assert.strictEqual(result.errors[2].context, '#'); 526 | }); 527 | }); 528 | }); 529 | }); 530 | --------------------------------------------------------------------------------