├── .editorconfig ├── .gitignore ├── .gitmodules ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── all.js ├── coercion.js ├── ref.js ├── shortcut.js └── simple.js ├── lib ├── attribute.js ├── helpers.js ├── index.d.ts ├── index.js ├── scan.js └── validator.js ├── package-lock.json ├── package.json ├── stryker.conf.js ├── test ├── Validator.test.js ├── arrays.js ├── attributes.js ├── combinators.js ├── const.js ├── fixtures │ ├── data.json │ ├── data_collection.json │ ├── data_collection_schema.json │ ├── data_schema.json │ └── types.json ├── formats.js ├── i18n.js ├── interface.test.js ├── loading.js ├── metaschema.js ├── mixed.js ├── objects.js ├── pure.js ├── required_with_ref.js ├── suite.js └── union.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js dependencies 2 | node_modules 3 | npm-debug.log 4 | 5 | # stryker output files 6 | .stryker-tmp 7 | reports/ 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/suite"] 2 | path = test/suite 3 | url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | docs/ 3 | examples/ 4 | test/ 5 | .DS_Store 6 | node_modules 7 | .idea 8 | .travis.yml 9 | stryker.conf.js 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 12 5 | - 14 6 | before_install: 7 | - git submodule update --init --recursive 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | jsonschema is licensed under MIT license. 2 | 3 | Copyright (C) 2012-2015 Tom de Grunt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/tdegrunt/jsonschema.svg)](http://travis-ci.org/tdegrunt/jsonschema) 2 | 3 | # jsonschema 4 | 5 | [JSON schema](http://json-schema.org/) validator, which is designed to be fast and simple to use. JSON Schema versions through draft-07 are fully supported. 6 | 7 | ## Contributing & bugs 8 | 9 | Please fork the repository, make the changes in your fork and include tests. Once you're done making changes, send in a pull request. 10 | 11 | ### Bug reports 12 | 13 | Please include a test which shows why the code fails. 14 | 15 | ## Usage 16 | 17 | ### Simple 18 | 19 | Simple object validation using JSON schemas. 20 | 21 | ```javascript 22 | var Validator = require('jsonschema').Validator; 23 | var v = new Validator(); 24 | var instance = 4; 25 | var schema = {"type": "number"}; 26 | console.log(v.validate(instance, schema)); 27 | ``` 28 | 29 | ### Even simpler 30 | 31 | ```javascript 32 | var validate = require('jsonschema').validate; 33 | console.log(validate(4, {"type": "number"})); 34 | ``` 35 | 36 | ### Complex example, with split schemas and references 37 | 38 | ```javascript 39 | var Validator = require('jsonschema').Validator; 40 | var v = new Validator(); 41 | 42 | // Address, to be embedded on Person 43 | var addressSchema = { 44 | "id": "/SimpleAddress", 45 | "type": "object", 46 | "properties": { 47 | "lines": { 48 | "type": "array", 49 | "items": {"type": "string"} 50 | }, 51 | "zip": {"type": "string"}, 52 | "city": {"type": "string"}, 53 | "country": {"type": "string"} 54 | }, 55 | "required": ["country"] 56 | }; 57 | 58 | // Person 59 | var schema = { 60 | "id": "/SimplePerson", 61 | "type": "object", 62 | "properties": { 63 | "name": {"type": "string"}, 64 | "address": {"$ref": "/SimpleAddress"}, 65 | "votes": {"type": "integer", "minimum": 1} 66 | } 67 | }; 68 | 69 | var p = { 70 | "name": "Barack Obama", 71 | "address": { 72 | "lines": [ "1600 Pennsylvania Avenue Northwest" ], 73 | "zip": "DC 20500", 74 | "city": "Washington", 75 | "country": "USA" 76 | }, 77 | "votes": "lots" 78 | }; 79 | 80 | v.addSchema(addressSchema, '/SimpleAddress'); 81 | console.log(v.validate(p, schema)); 82 | ``` 83 | Returned ValidatorResult object, will show this example is NOT valid since: `"votes": "lots"` is not an integer. 84 | 85 | ### Example for Array schema 86 | 87 | ```json 88 | var arraySchema = { 89 | "type": "array", 90 | "items": { 91 | "properties": { 92 | "name": { "type": "string" }, 93 | "lastname": { "type": "string" } 94 | }, 95 | "required": ["name", "lastname"] 96 | } 97 | } 98 | ``` 99 | For a comprehensive, annotated example illustrating all possible validation options, see [examples/all.js](./examples/all.js) 100 | 101 | ## Features 102 | 103 | ### Definitions 104 | 105 | All schema definitions are supported, $schema is ignored. 106 | 107 | ### Types 108 | 109 | All types are supported 110 | 111 | ### Handling `undefined` 112 | 113 | `undefined` is not a value known to JSON, and by default, the validator treats it as if it is not invalid. i.e., it will return valid. 114 | 115 | ```javascript 116 | var res = validate(undefined, {type: 'string'}); 117 | res.valid // true 118 | ``` 119 | 120 | This behavior may be changed with the "required" option: 121 | 122 | ```javascript 123 | var res = validate(undefined, {type: 'string'}, {required: true}); 124 | res.valid // false 125 | ``` 126 | 127 | ### Formats 128 | 129 | #### Disabling the format keyword. 130 | 131 | You may disable format validation by providing `disableFormat: true` to the validator 132 | options. 133 | 134 | #### String Formats 135 | 136 | All formats are supported, phone numbers are expected to follow the [E.123](http://en.wikipedia.org/wiki/E.123) standard. 137 | 138 | #### Custom Formats 139 | 140 | You may add your own custom format functions. Format functions accept the input 141 | being validated and return a boolean value. If the returned value is `true`, then 142 | validation succeeds. If the returned value is `false`, then validation fails. 143 | 144 | * Formats added to `Validator.prototype.customFormats` do not affect previously instantiated 145 | Validators. This is to prevent validator instances from being altered once created. 146 | It is conceivable that multiple validators may be created to handle multiple schemas 147 | with different formats in a program. 148 | * Formats added to `validator.customFormats` affect only that Validator instance. 149 | 150 | Here is an example that uses custom formats: 151 | 152 | ```javascript 153 | Validator.prototype.customFormats.myFormat = function(input) { 154 | return input === 'myFormat'; 155 | }; 156 | 157 | var validator = new Validator(); 158 | validator.validate('myFormat', {type: 'string', format: 'myFormat'}).valid; // true 159 | validator.validate('foo', {type: 'string', format: 'myFormat'}).valid; // false 160 | ``` 161 | 162 | ### Results 163 | 164 | By default, results will be returned in a `ValidatorResult` object with the following properties: 165 | 166 | * `instance`: any. 167 | * `schema`: Schema. 168 | * `errors`: ValidationError[]. 169 | * `valid`: boolean. 170 | 171 | Each item in `errors` is a `ValidationError` with the following properties: 172 | 173 | * path: array. An array of property keys or array offsets, indicating where inside objects or arrays the instance was found. 174 | * property: string. Describes the property path. Starts with `instance`, and is delimited with a dot (`.`). 175 | * message: string. A human-readable message for debugging use. Provided in English and subject to change. 176 | * schema: object. The schema containing the keyword that failed 177 | * instance: any. The instance that failed 178 | * name: string. The keyword within the schema that failed. 179 | * argument: any. Provides information about the keyword that failed. 180 | 181 | The validator can be configured to throw in the event of a validation error: 182 | 183 | * If the `throwFirst` option is set, the validator will terminate validation at the first encountered error and throw a `ValidatorResultError` object. 184 | 185 | * If the `throwAll` option is set, the validator will throw a `ValidatorResultError` object after the entire instance has been validated. 186 | 187 | * If the `throwError` option is set, it will throw at the first encountered validation error (like `throwFirst`), but the `ValidationError` object itself will be thrown. Note that, despite the name, this does not inherit from Error like `ValidatorResultError` does. 188 | 189 | The `ValidatorResultError` object has the same properties as `ValidatorResult` and additionally inherits from Error. 190 | 191 | #### "nestedErrors" option 192 | 193 | When `oneOf` or `anyOf` validations fail, errors that caused any of the sub-schemas referenced therein to fail are normally suppressed, because it is not necessary to fix all of them. And in the case of `oneOf`, it would itself be an error to fix all of the listed errors. 194 | 195 | This behavior may be configured with `options.nestedErrors`. If truthy, it will emit all the errors from the subschemas. This option may be useful when troubleshooting validation errors in complex schemas: 196 | 197 | ```javascript 198 | var schema = { 199 | oneOf: [ 200 | { type: 'string', minLength: 32, maxLength: 32 }, 201 | { type: 'string', maxLength: 16 }, 202 | { type: 'number' }, 203 | ] 204 | }; 205 | var validator = new Validator(); 206 | var result = validator.validate('This string is 28 chars long', schema, {nestedErrors: true}); 207 | 208 | // result.toString() reads out: 209 | // 0: instance does not meet minimum length of 32 210 | // 1: instance does not meet maximum length of 16 211 | // 2: instance is not of a type(s) number 212 | // 3: instance is not exactly one from [subschema 0],[subschema 1],[subschema 2] 213 | ``` 214 | 215 | #### Localizing Error Messages 216 | 217 | To provide localized, human-readable errors, use the `name` string as a translation key. Feel free to open an issue for support relating to localizing error messages. For example: 218 | 219 | ``` 220 | var localized = result.errors.map(function(err){ 221 | return localeService.translate(err.name); 222 | }); 223 | ``` 224 | 225 | ### Custom keywords 226 | 227 | Specify your own JSON Schema keywords with the validator.attributes property: 228 | 229 | ```javascript 230 | validator.attributes.contains = function validateContains(instance, schema, options, ctx) { 231 | if(typeof instance !== 'string') return; 232 | if(typeof schema.contains !== 'string') throw new jsonschema.SchemaError('"contains" expects a string', schema); 233 | if(instance.indexOf(schema.contains)<0){ 234 | return 'does not contain the string ' + JSON.stringify(schema.contains); 235 | } 236 | } 237 | var result = validator.validate("I am an instance", { type:"string", contains: "I am" }); 238 | // result.valid === true; 239 | ``` 240 | 241 | The instance passes validation if the function returns nothing. A single validation error is produced 242 | if the function returns a string. Any number of errors (maybe none at all) may be returned by passing a 243 | `ValidatorResult` object, which may be used like so: 244 | 245 | ```javascript 246 | var result = new ValidatorResult(instance, schema, options, ctx); 247 | while(someErrorCondition()){ 248 | result.addError('fails some validation test'); 249 | } 250 | return result; 251 | ``` 252 | 253 | ### Dereferencing schemas 254 | 255 | Sometimes you may want to download schemas from remote sources, like a database, or over HTTP. When importing a schema, 256 | unknown references are inserted into the `validator.unresolvedRefs` Array. Asynchronously shift elements off this array and import 257 | them: 258 | 259 | ```javascript 260 | var Validator = require('jsonschema').Validator; 261 | var v = new Validator(); 262 | v.addSchema(initialSchema); 263 | function importNextSchema(){ 264 | var nextSchema = v.unresolvedRefs.shift(); 265 | if(!nextSchema){ done(); return; } 266 | databaseGet(nextSchema, function(schema){ 267 | v.addSchema(schema); 268 | importNextSchema(); 269 | }); 270 | } 271 | importNextSchema(); 272 | ``` 273 | ### Disallowing unknown attributes 274 | 275 | Sometimes you may want to disallow unknown attributes passed in the body of the request, in order to disallow those unknown attributes before the validation of the body, you need to set additionalProperties to false. 276 | 277 | ```javascript 278 | { 279 | "$schema": "http://json-schema.org/draft-04/schema#", 280 | "title": "Accounting Resource - Add Item", 281 | "type": "object", 282 | "additionalProperties": false, 283 | "properties": { 284 | "itemNumber": { 285 | "type":"string" 286 | }, 287 | "title": { 288 | "type":"string" 289 | }, 290 | "description": { 291 | "type":"string" 292 | } 293 | }, 294 | "required": [ 295 | "itemNumber", 296 | "title", 297 | "description" 298 | ] 299 | } 300 | ``` 301 | 302 | ### Default base URI 303 | 304 | Schemas should typically have an `id` with an absolute, full URI. However if the schema you are using contains only relative URI references, the `base` option will be used to resolve these. 305 | 306 | This following example would throw a `SchemaError` if the `base` option were unset: 307 | 308 | ```javascript 309 | var result = validate(["Name"], { 310 | id: "/schema.json", 311 | type: "array", 312 | items: { $ref: "http://example.com/schema.json#/definitions/item" }, 313 | definitions: { 314 | item: { type: "string" }, 315 | }, 316 | }, { base: 'http://example.com/' }); 317 | ``` 318 | 319 | ### Rewrite Hook 320 | 321 | The `rewrite` option lets you change the value of an instance after it has successfully been validated. This will mutate the `instance` passed to the validate function. This can be useful for unmarshalling data and parsing it into native instances, such as changing a string to a `Date` instance. 322 | 323 | The `rewrite` option accepts a function with the following arguments: 324 | 325 | * instance: any 326 | * schema: object 327 | * options: object 328 | * ctx: object 329 | * return value: any new value for the instance 330 | 331 | The value may be removed by returning `undefined`. 332 | If you don't want to change the value, call `return instance`. 333 | 334 | Here is an example that can convert a property expecting a date into a Date instance: 335 | 336 | ```javascript 337 | const schema = { 338 | properties: { 339 | date: {id: 'http://example.com/date', type: 'string'}, 340 | }, 341 | }; 342 | 343 | const value = { 344 | date: '2020-09-30T23:39:27.060Z', 345 | }; 346 | 347 | function unmarshall(instance, schema){ 348 | if(schema.id === 'http://example.com/date'){ 349 | return new Date(instance); 350 | } 351 | return instance; 352 | } 353 | 354 | const v = new Validator(); 355 | const res = v.validate(value, schema, {rewrite: unmarshall}); 356 | 357 | assert(res.instance.date instanceof Date); 358 | ``` 359 | 360 | 361 | ### Pre-Property Validation Hook 362 | 363 | If some processing of properties is required prior to validation a function may be passed via the options parameter of the validate function. For example, say you needed to perform type coercion for some properties: 364 | 365 | ```javascript 366 | // See examples/coercion.js 367 | function preValidateProperty(object, key, schema, options, ctx) { 368 | var value = object[key]; 369 | if (typeof value === 'undefined') return; 370 | 371 | // Test if the schema declares a type, but the type keyword fails validation 372 | if (schema.type && validator.attributes.type.call(validator, value, schema, options, ctx.makeChild(schema, key))) { 373 | // If the type is "number" but the instance is not a number, cast it 374 | if(schema.type==='number' && typeof value!=='number'){ 375 | object[key] = parseFloat(value); 376 | return; 377 | } 378 | // If the type is "string" but the instance is not a string, cast it 379 | if(schema.type==='string' && typeof value!=='string'){ 380 | object[key] = String(value).toString(); 381 | return; 382 | } 383 | } 384 | }; 385 | 386 | // And now, to actually perform validation with the coercion hook! 387 | v.validate(instance, schema, { preValidateProperty }); 388 | ``` 389 | 390 | ### Skip validation of certain keywords 391 | 392 | Use the "skipAttributes" option to skip validation of certain keywords. Provide an array of keywords to ignore. 393 | 394 | For skipping the "format" keyword, see the disableFormat option. 395 | 396 | ### Fail on unknown keywords 397 | 398 | By default, JSON Schema is supposed to ignore unknown schema keywords. 399 | 400 | You can change this behavior to require that all keywords used in a schema have a defined behavior, by using setting the "allowUnknownAttributes" option to false. 401 | 402 | This example will throw a `SchemaError`: 403 | 404 | ```javascript 405 | var schema = { 406 | type: "string", 407 | format: "email", 408 | example: "foo", 409 | }; 410 | var result = validate("Name", schema, { allowUnknownAttributes: false }); 411 | ``` 412 | 413 | ## Tests 414 | 415 | Uses [JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite) as well as our own tests. 416 | You'll need to update and init the git submodules: 417 | 418 | git submodule update --init 419 | npm test 420 | 421 | ## Contributions 422 | 423 | This library would not be possible without the valuable contributions by: 424 | 425 | - Austin Wright 426 | 427 | ... and many others! 428 | 429 | ## License 430 | 431 | jsonschema is licensed under MIT license. 432 | 433 | Copyright (C) 2012-2025 Tom de Grunt 434 | 435 | Permission is hereby granted, free of charge, to any person obtaining a copy of 436 | this software and associated documentation files (the "Software"), to deal in 437 | the Software without restriction, including without limitation the rights to 438 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 439 | of the Software, and to permit persons to whom the Software is furnished to do 440 | so, subject to the following conditions: 441 | 442 | The above copyright notice and this permission notice shall be included in all 443 | copies or substantial portions of the Software. 444 | 445 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 446 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 447 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 448 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 449 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 450 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 451 | SOFTWARE. 452 | -------------------------------------------------------------------------------- /examples/all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Validator = require('jsonschema').Validator; 4 | 5 | // This schema includes all of the possible validation tests. 6 | var schema = { 7 | "id": "/All", 8 | "type": "object", 9 | "properties": { 10 | // -------------------------------------------------------------------- 11 | // Type. 12 | // -------------------------------------------------------------------- 13 | // 14 | // Validate that this property is of a specific type. Types are not the 15 | // standard set obtained from typeof, however. They are: 16 | // 17 | // string, number, integer, boolean, array, null, date, any, object. 18 | "validateType1": { 19 | "type": "string" 20 | }, 21 | // Note that multiple type options can be provided in an array. 22 | "validateType2": { 23 | "type": ["boolean", "string"] 24 | }, 25 | // It is also possible to specify forbidden types, either using "not" or 26 | // "disallow", either of which can accept a string type name or array of 27 | // string type names. 28 | "validateType3": { 29 | "not": "string", 30 | }, 31 | "validateType4": { 32 | "disallow": ["boolean", "string"] 33 | }, 34 | 35 | // -------------------------------------------------------------------- 36 | // Object Properties. 37 | // -------------------------------------------------------------------- 38 | // 39 | // Describing properties is the way in which a schema for nested objects can 40 | // be constructed. 41 | // 42 | // Firstly any number of object properties can be described one by one, by 43 | // name, and given validation rules. 44 | "validateProperties": { 45 | "type": "object", 46 | "properties": { 47 | "a": { 48 | "type": "array" 49 | }, 50 | "b": { 51 | "type": "integer" 52 | } 53 | } 54 | }, 55 | // Secondly, regular expression strings can be used to describe validation 56 | // of properties. 57 | "validatePatternProperties": { 58 | "type": "object", 59 | "patternProperties": { 60 | // The property name will be passed to new RegExp(prop), so backslashes 61 | // have to be escaped. 62 | "^x1\\d*$": { 63 | "type": "array" 64 | }, 65 | "^[a-z]+$": { 66 | "type": "integer" 67 | } 68 | } 69 | }, 70 | // A catch-all additionalProperties validation is applied to every property 71 | // not defined explicitly or matched by a pattern. 72 | "validateAdditionalProperties1": { 73 | "type": "object", 74 | "properties": { 75 | "a": { 76 | "type": "array" 77 | } 78 | }, 79 | "patternProperties": { 80 | "[b-z]+": { 81 | "type": "integer" 82 | } 83 | }, 84 | "additionalProperties": { 85 | "type": "boolean" 86 | } 87 | }, 88 | // If additionalProperties is set false then the presence of any properties 89 | // other than those specified will fail validation. 90 | "validateAdditionalProperties2": { 91 | "type": "object", 92 | "properties": { 93 | "a": { 94 | "type": "array" 95 | } 96 | }, 97 | "patternProperties": { 98 | "[b-z]+": { 99 | "type": "integer" 100 | } 101 | }, 102 | "additionalProperties": false 103 | }, 104 | // The number of properties present on the object can also be validated. 105 | "validateNumberOfProperties": { 106 | "type": "object", 107 | "patternProperties": { 108 | "[a-z]+": { 109 | "type": "string" 110 | } 111 | }, 112 | "minProperties": 1, 113 | "maxProperties": 2 114 | }, 115 | 116 | // -------------------------------------------------------------------- 117 | // Required. 118 | // -------------------------------------------------------------------- 119 | // 120 | // All schema validation is optional unless defined to be required. In 121 | // other words, a value of undefined will pass validation. 122 | // 123 | // In this simple case, any value other than undefined is acceptable. 124 | "validateRequired": { 125 | "type": "any", 126 | "required": true 127 | }, 128 | // For objects, it is also possible to list property names that are 129 | // required. This can be done in two ways, shown below: 130 | "validateRequiredProperties1": { 131 | "type": "object", 132 | "required": [ 133 | "propA", 134 | "propB" 135 | ] 136 | }, 137 | "validateRequiredProperties2": { 138 | "type": "object", 139 | "properties": { 140 | "propA": { 141 | "required": true 142 | }, 143 | "propB": { 144 | "required": true 145 | } 146 | } 147 | }, 148 | 149 | // -------------------------------------------------------------------- 150 | // Arrays. 151 | // -------------------------------------------------------------------- 152 | // 153 | // The elements of an array can be validated to match any schema definition, 154 | // either simple or as complex and nested as desired. 155 | "validateItems": { 156 | "type": "array", 157 | // A simple schema for the items in this array, only specifying type. 158 | "items": { 159 | "type": "string" 160 | } 161 | }, 162 | // The size of the array can also be validated. 163 | "validateNumberOfItems": { 164 | "type": "array", 165 | "minItems": 1, 166 | "maxItems": 2 167 | }, 168 | // If the uniqueItems property is defined with any value, then validation 169 | // fails if any of the items in the array pass a strict equality test. 170 | "validateUniqueItems": { 171 | "type": "array", 172 | "uniqueItems": true 173 | }, 174 | 175 | // -------------------------------------------------------------------- 176 | // Numbers. 177 | // -------------------------------------------------------------------- 178 | // 179 | // Number and integer types can be validated in a number of ways: 180 | "validateNumber": { 181 | "type": ["number"], 182 | "minimum": 0, 183 | "maximum": 10, 184 | // Beware floating point errors! 185 | "divisibleBy": 2.4, 186 | "multipleOf": 4.8 187 | }, 188 | // A few of the options are more applicable to integers: 189 | "validateInteger": { 190 | "type": ["integer"], 191 | // Only even numbers pass validation. 192 | "divisibleBy": 2, 193 | "multipleOf": 4 194 | }, 195 | 196 | // -------------------------------------------------------------------- 197 | // Strings. 198 | // -------------------------------------------------------------------- 199 | // 200 | // Match against a substring or regular expression. 201 | "validatePattern1": { 202 | "type": "string", 203 | "pattern": "str" 204 | }, 205 | "validatePattern2": { 206 | "type": "string", 207 | "pattern": /str/ 208 | }, 209 | // The format validation option provides shortcuts for various regular 210 | // expressions or functions that check specific string formats. The 211 | // available formats are: 212 | // 213 | // date-time, date, time, ip-address, ipv6, uri, color, host-name, alpha, 214 | // alpha-numeric, utc-millisec 215 | "validateFormat": { 216 | "type": "string", 217 | // This validates against possible values for color in CSS. 218 | "format": "color" 219 | }, 220 | // String length can be validated. 221 | "validateLength": { 222 | "type": "string", 223 | "minLength": 1, 224 | "maxLength": 2 225 | }, 226 | 227 | // -------------------------------------------------------------------- 228 | // Enumerated values. 229 | // -------------------------------------------------------------------- 230 | // 231 | // Validation passes if the property is strictly equal to one of the 232 | // enumerated values. 233 | "validateEnum": { 234 | "enum": [ 235 | "value", 236 | { 237 | "x": 11 238 | } 239 | ] 240 | }, 241 | 242 | // -------------------------------------------------------------------- 243 | // Dependencies. 244 | // -------------------------------------------------------------------- 245 | // 246 | // It is possible to declare a property in an object to require the presence 247 | // of one or more other properties. 248 | // 249 | // In this case neither "a" nor "b" are defined as required, but if "a" is 250 | // present, then "b" must also be present. 251 | "validateDependencies1": { 252 | "type": "object", 253 | "properties": { 254 | "a": { 255 | "type": "string", 256 | }, 257 | "b": { 258 | "type": "boolean" 259 | } 260 | }, 261 | "dependencies": { 262 | "a": "b" 263 | } 264 | }, 265 | // Multiple dependencies can be defined for any one property. Here again 266 | // none of the properties are defined as being required, but if "a" is 267 | // present then "b" and "c" must also be present. 268 | "validateDependencies2": { 269 | "type": "object", 270 | "properties": { 271 | "a": { 272 | "type": "string", 273 | }, 274 | "b": { 275 | "type": "boolean" 276 | }, 277 | "c": { 278 | "type": "number" 279 | } 280 | }, 281 | "dependencies": { 282 | "a": ["b", "c"] 283 | } 284 | }, 285 | 286 | // -------------------------------------------------------------------- 287 | // Schema matching options. 288 | // -------------------------------------------------------------------- 289 | // 290 | // There are a number of options for validating against more than one 291 | // schema: one of, any of, all of. 292 | // 293 | // The property must match one or more of the validation schema provided in 294 | // the array, which can be as simple or complex and nested as desired. 295 | "validateAnyOf": { "anyOf" : [ 296 | { 297 | "type": "boolean" 298 | }, 299 | { 300 | "type": "string" 301 | } 302 | ]}, 303 | // The property must match all of the validation schema provided in the 304 | // array, which can be as simple or complex and nested as desired. 305 | "validateAllOf": { "allOf" : [ 306 | { 307 | "type": "boolean" 308 | }, 309 | { 310 | "enum": [true] 311 | } 312 | ]}, 313 | // The property must match only one of the validation schema provided in the 314 | // array, which can be as simple or complex and nested as desired. 315 | "validateOneOf": { "oneOf" : [ 316 | { 317 | "type": "boolean" 318 | }, 319 | { 320 | "type": "integer" 321 | } 322 | ]}, 323 | 324 | // -------------------------------------------------------------------- 325 | // References. 326 | // -------------------------------------------------------------------- 327 | // 328 | // One schema definition can reference other schema definitions, which 329 | // allows easier construction of more complex schemas by reusing their 330 | // common component parts. 331 | "validateReference": { 332 | // The /ReferencedSchema is defined below in the referencedSchema 333 | // variable. It must be registered with the validator prior to validation 334 | // using the addSchema method. See below for that as well. 335 | "$ref": "/ReferencedSchema" 336 | } 337 | } 338 | }; 339 | 340 | // An example of a smaller schema referenced from the main schema definition. 341 | // This is about as simple as a schema can possibly be - most are more complex, 342 | // describing common collections of data such as addresses or database rows. 343 | var referencedSchema = { 344 | "id": "/ReferencedSchema", 345 | "type": "string" 346 | }; 347 | 348 | var all = { 349 | "validateType1": "a string", 350 | "validateType2": true, 351 | "validateType3": 6, 352 | "validateType4": 6, 353 | "validateProperties": { 354 | "a": [], 355 | "b": 6 356 | }, 357 | "validatePatternProperties": { 358 | "x11": [], 359 | "abc": 5 360 | }, 361 | "validateAdditionalProperties1": { 362 | "a": [], 363 | "bcd": 4, 364 | "11": true 365 | }, 366 | "validateAdditionalProperties2": { 367 | "a": [], 368 | "bcd": 4 369 | }, 370 | "validateNumberOfProperties": { 371 | "abc": "a string" 372 | }, 373 | "validateRequired": 6, 374 | "validateRequiredProperties1": { 375 | "propA": 6, 376 | "propB": "a string" 377 | }, 378 | "validateRequiredProperties2": { 379 | "propA": 6, 380 | "propB": "a string" 381 | }, 382 | "validateItems": [ 383 | "str-a", 384 | "str-b" 385 | ], 386 | "validateNumberOfItems": [ 387 | "str-a" 388 | ], 389 | "validateUniqueItems": [ 390 | "str-a", 391 | "str-b" 392 | ], 393 | "validateNumber": 9.6, 394 | "validateInteger": 8, 395 | "validatePattern1": "a string", 396 | "validatePattern2": "a string", 397 | "validateFormat": "blue", 398 | "validateLength": "a", 399 | "validateEnum": { 400 | "x": 11 401 | }, 402 | "validateDependencies1": { 403 | "a": "a string", 404 | "b": true 405 | }, 406 | "validateDependencies2": { 407 | "a": "a string", 408 | "b": true, 409 | "c": 8 410 | }, 411 | "validateAnyOf": "a string", 412 | "validateAllOf": true, 413 | "validateOneOf": 6, 414 | "validateReference": "a string" 415 | }; 416 | 417 | var v = new Validator(); 418 | v.addSchema(referencedSchema, '/ReferencedSchema'); 419 | console.log(v.validate(all, schema)); 420 | -------------------------------------------------------------------------------- /examples/coercion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jsonschema = require('..'); 4 | var validator = new jsonschema.Validator; 5 | 6 | function preValidateProperty(object, key, schema, options, ctx) { 7 | var value = object[key]; 8 | if (typeof value === 'undefined') return; 9 | 10 | // Test if the schema declares a type, but the type keyword fails validation 11 | if (schema.type && validator.attributes.type.call(validator, value, schema, options, ctx.makeChild(schema, key))) { 12 | // If the type is "number" but the instance is not a number, cast it 13 | if(schema.type==='number' && typeof value!=='number'){ 14 | object[key] = parseFloat(value); 15 | return; 16 | } 17 | // If the type is "string" but the instance is not a string, cast it 18 | if(schema.type==='string' && typeof value!=='string'){ 19 | object[key] = String(value).toString(); 20 | return; 21 | } 22 | } 23 | } 24 | 25 | const schema = { 26 | properties: { 27 | name: { type: 'string' }, 28 | quantity: { type: 'number' }, 29 | }, 30 | }; 31 | 32 | const instance = { 33 | name: 123, 34 | quantity: "2", 35 | }; 36 | 37 | // And now, to actually perform validation with the coercion hook! 38 | var res = validator.validate(instance, schema, { preValidateProperty }); 39 | 40 | console.log(instance); 41 | console.log(res.valid); 42 | -------------------------------------------------------------------------------- /examples/ref.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var Validator = require('jsonschema').Validator; 5 | 6 | // Address, to be embedded on Person 7 | var addressSchema = { 8 | "id": "/SimpleAddress", 9 | "type": "object", 10 | "properties": { 11 | "lines": { 12 | "type": "array", 13 | "items": {"type": "string"} 14 | }, 15 | "zip": {"type": "string"}, 16 | "city": {"type": "string"}, 17 | "country": {"type": "string", "required": true} 18 | } 19 | }; 20 | 21 | // Person model 22 | var schema = { 23 | "id": "/SimplePerson", 24 | "type": "object", 25 | "properties": { 26 | "name": {"type": "string"}, 27 | "address": {"$ref": "/SimpleAddress"}, 28 | "votes": {"type": "integer", "minimum": 1} 29 | } 30 | }; 31 | 32 | var p = { 33 | "name": "Barack Obama", 34 | "address": { 35 | "lines": [ "1600 Pennsylvania Avenue Northwest" ], 36 | "zip": "DC 20500", 37 | "city": "Washington", 38 | "country": "USA" 39 | }, 40 | "votes": "lots" 41 | }; 42 | 43 | var v = new Validator(); 44 | v.addSchema(addressSchema, '/SimpleAddress'); 45 | console.log(v.validate(p, schema)); 46 | -------------------------------------------------------------------------------- /examples/shortcut.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var validate = require('jsonschema').validate; 4 | 5 | var instance = 4; 6 | var schema = {"type": "number"}; 7 | console.log(validate(instance, schema)); 8 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var Validator = require('jsonschema').Validator; 5 | 6 | var v = new Validator(); 7 | var instance = 4; 8 | var schema = {"type": "number"}; 9 | console.log(v.validate(instance, schema)); 10 | -------------------------------------------------------------------------------- /lib/attribute.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var helpers = require('./helpers'); 4 | 5 | /** @type ValidatorResult */ 6 | var ValidatorResult = helpers.ValidatorResult; 7 | /** @type SchemaError */ 8 | var SchemaError = helpers.SchemaError; 9 | 10 | var attribute = {}; 11 | 12 | attribute.ignoreProperties = { 13 | // informative properties 14 | 'id': true, 15 | 'default': true, 16 | 'description': true, 17 | 'title': true, 18 | // arguments to other properties 19 | 'additionalItems': true, 20 | 'then': true, 21 | 'else': true, 22 | // special-handled properties 23 | '$schema': true, 24 | '$ref': true, 25 | 'extends': true, 26 | }; 27 | 28 | /** 29 | * @name validators 30 | */ 31 | var validators = attribute.validators = {}; 32 | 33 | /** 34 | * Validates whether the instance is of a certain type 35 | * @param instance 36 | * @param schema 37 | * @param options 38 | * @param ctx 39 | * @return {ValidatorResult|null} 40 | */ 41 | validators.type = function validateType (instance, schema, options, ctx) { 42 | // Ignore undefined instances 43 | if (instance === undefined) { 44 | return null; 45 | } 46 | var result = new ValidatorResult(instance, schema, options, ctx); 47 | var types = Array.isArray(schema.type) ? schema.type : [schema.type]; 48 | if (!types.some(this.testType.bind(this, instance, schema, options, ctx))) { 49 | var list = types.map(function (v) { 50 | if(!v) return; 51 | var id = v.$id || v.id; 52 | return id ? ('<' + id + '>') : (v+''); 53 | }); 54 | result.addError({ 55 | name: 'type', 56 | argument: list, 57 | message: "is not of a type(s) " + list, 58 | }); 59 | } 60 | return result; 61 | }; 62 | 63 | function testSchemaNoThrow(instance, options, ctx, callback, schema){ 64 | var throwError = options.throwError; 65 | var throwAll = options.throwAll; 66 | options.throwError = false; 67 | options.throwAll = false; 68 | var res = this.validateSchema(instance, schema, options, ctx); 69 | options.throwError = throwError; 70 | options.throwAll = throwAll; 71 | 72 | if (!res.valid && callback instanceof Function) { 73 | callback(res); 74 | } 75 | return res.valid; 76 | } 77 | 78 | /** 79 | * Validates whether the instance matches some of the given schemas 80 | * @param instance 81 | * @param schema 82 | * @param options 83 | * @param ctx 84 | * @return {ValidatorResult|null} 85 | */ 86 | validators.anyOf = function validateAnyOf (instance, schema, options, ctx) { 87 | // Ignore undefined instances 88 | if (instance === undefined) { 89 | return null; 90 | } 91 | var result = new ValidatorResult(instance, schema, options, ctx); 92 | var inner = new ValidatorResult(instance, schema, options, ctx); 93 | if (!Array.isArray(schema.anyOf)){ 94 | throw new SchemaError("anyOf must be an array"); 95 | } 96 | if (!schema.anyOf.some( 97 | testSchemaNoThrow.bind( 98 | this, instance, options, ctx, function(res){inner.importErrors(res);} 99 | ))) { 100 | var list = schema.anyOf.map(function (v, i) { 101 | var id = v.$id || v.id; 102 | if(id) return '<' + id + '>'; 103 | return(v.title && JSON.stringify(v.title)) || (v['$ref'] && ('<' + v['$ref'] + '>')) || '[subschema '+i+']'; 104 | }); 105 | if (options.nestedErrors) { 106 | result.importErrors(inner); 107 | } 108 | result.addError({ 109 | name: 'anyOf', 110 | argument: list, 111 | message: "is not any of " + list.join(','), 112 | }); 113 | } 114 | return result; 115 | }; 116 | 117 | /** 118 | * Validates whether the instance matches every given schema 119 | * @param instance 120 | * @param schema 121 | * @param options 122 | * @param ctx 123 | * @return {String|null} 124 | */ 125 | validators.allOf = function validateAllOf (instance, schema, options, ctx) { 126 | // Ignore undefined instances 127 | if (instance === undefined) { 128 | return null; 129 | } 130 | if (!Array.isArray(schema.allOf)){ 131 | throw new SchemaError("allOf must be an array"); 132 | } 133 | var result = new ValidatorResult(instance, schema, options, ctx); 134 | var self = this; 135 | schema.allOf.forEach(function(v, i){ 136 | var valid = self.validateSchema(instance, v, options, ctx); 137 | if(!valid.valid){ 138 | var id = v.$id || v.id; 139 | var msg = id || (v.title && JSON.stringify(v.title)) || (v['$ref'] && ('<' + v['$ref'] + '>')) || '[subschema '+i+']'; 140 | result.addError({ 141 | name: 'allOf', 142 | argument: { id: msg, length: valid.errors.length, valid: valid }, 143 | message: 'does not match allOf schema ' + msg + ' with ' + valid.errors.length + ' error[s]:', 144 | }); 145 | result.importErrors(valid); 146 | } 147 | }); 148 | return result; 149 | }; 150 | 151 | /** 152 | * Validates whether the instance matches exactly one of the given schemas 153 | * @param instance 154 | * @param schema 155 | * @param options 156 | * @param ctx 157 | * @return {String|null} 158 | */ 159 | validators.oneOf = function validateOneOf (instance, schema, options, ctx) { 160 | // Ignore undefined instances 161 | if (instance === undefined) { 162 | return null; 163 | } 164 | if (!Array.isArray(schema.oneOf)){ 165 | throw new SchemaError("oneOf must be an array"); 166 | } 167 | var result = new ValidatorResult(instance, schema, options, ctx); 168 | var inner = new ValidatorResult(instance, schema, options, ctx); 169 | var count = schema.oneOf.filter( 170 | testSchemaNoThrow.bind( 171 | this, instance, options, ctx, function(res) {inner.importErrors(res);} 172 | ) ).length; 173 | var list = schema.oneOf.map(function (v, i) { 174 | var id = v.$id || v.id; 175 | return id || (v.title && JSON.stringify(v.title)) || (v['$ref'] && ('<' + v['$ref'] + '>')) || '[subschema '+i+']'; 176 | }); 177 | if (count!==1) { 178 | if (options.nestedErrors) { 179 | result.importErrors(inner); 180 | } 181 | result.addError({ 182 | name: 'oneOf', 183 | argument: list, 184 | message: "is not exactly one from " + list.join(','), 185 | }); 186 | } 187 | return result; 188 | }; 189 | 190 | /** 191 | * Validates "then" or "else" depending on the result of validating "if" 192 | * @param instance 193 | * @param schema 194 | * @param options 195 | * @param ctx 196 | * @return {String|null} 197 | */ 198 | validators.if = function validateIf (instance, schema, options, ctx) { 199 | // Ignore undefined instances 200 | if (instance === undefined) return null; 201 | if (!helpers.isSchema(schema.if)) throw new Error('Expected "if" keyword to be a schema'); 202 | var ifValid = testSchemaNoThrow.call(this, instance, options, ctx, null, schema.if); 203 | var result = new ValidatorResult(instance, schema, options, ctx); 204 | var res; 205 | if(ifValid){ 206 | if (schema.then === undefined) return; 207 | if (!helpers.isSchema(schema.then)) throw new Error('Expected "then" keyword to be a schema'); 208 | res = this.validateSchema(instance, schema.then, options, ctx.makeChild(schema.then)); 209 | result.importErrors(res); 210 | }else{ 211 | if (schema.else === undefined) return; 212 | if (!helpers.isSchema(schema.else)) throw new Error('Expected "else" keyword to be a schema'); 213 | res = this.validateSchema(instance, schema.else, options, ctx.makeChild(schema.else)); 214 | result.importErrors(res); 215 | } 216 | return result; 217 | }; 218 | 219 | function getEnumerableProperty(object, key){ 220 | // Determine if `key` shows up in `for(var key in object)` 221 | // First test Object.hasOwnProperty.call as an optimization: that guarantees it does 222 | if(Object.hasOwnProperty.call(object, key)) return object[key]; 223 | // Test `key in object` as an optimization; false means it won't 224 | if(!(key in object)) return; 225 | while( (object = Object.getPrototypeOf(object)) ){ 226 | if(Object.propertyIsEnumerable.call(object, key)) return object[key]; 227 | } 228 | } 229 | 230 | /** 231 | * Validates propertyNames 232 | * @param instance 233 | * @param schema 234 | * @param options 235 | * @param ctx 236 | * @return {String|null|ValidatorResult} 237 | */ 238 | validators.propertyNames = function validatePropertyNames (instance, schema, options, ctx) { 239 | if(!this.types.object(instance)) return; 240 | var result = new ValidatorResult(instance, schema, options, ctx); 241 | var subschema = schema.propertyNames!==undefined ? schema.propertyNames : {}; 242 | if(!helpers.isSchema(subschema)) throw new SchemaError('Expected "propertyNames" to be a schema (object or boolean)'); 243 | 244 | for (var property in instance) { 245 | if(getEnumerableProperty(instance, property) !== undefined){ 246 | var res = this.validateSchema(property, subschema, options, ctx.makeChild(subschema)); 247 | result.importErrors(res); 248 | } 249 | } 250 | 251 | return result; 252 | }; 253 | 254 | /** 255 | * Validates properties 256 | * @param instance 257 | * @param schema 258 | * @param options 259 | * @param ctx 260 | * @return {String|null|ValidatorResult} 261 | */ 262 | validators.properties = function validateProperties (instance, schema, options, ctx) { 263 | if(!this.types.object(instance)) return; 264 | var result = new ValidatorResult(instance, schema, options, ctx); 265 | var properties = schema.properties || {}; 266 | for (var property in properties) { 267 | var subschema = properties[property]; 268 | if(subschema===undefined){ 269 | continue; 270 | }else if(subschema===null){ 271 | throw new SchemaError('Unexpected null, expected schema in "properties"'); 272 | } 273 | if (typeof options.preValidateProperty == 'function') { 274 | options.preValidateProperty(instance, property, subschema, options, ctx); 275 | } 276 | var prop = getEnumerableProperty(instance, property); 277 | var res = this.validateSchema(prop, subschema, options, ctx.makeChild(subschema, property)); 278 | if(res.instance !== result.instance[property]) result.instance[property] = res.instance; 279 | result.importErrors(res); 280 | } 281 | return result; 282 | }; 283 | 284 | /** 285 | * Test a specific property within in instance against the additionalProperties schema attribute 286 | * This ignores properties with definitions in the properties schema attribute, but no other attributes. 287 | * If too many more types of property-existence tests pop up they may need their own class of tests (like `type` has) 288 | * @private 289 | * @return {boolean} 290 | */ 291 | function testAdditionalProperty (instance, schema, options, ctx, property, result) { 292 | if(!this.types.object(instance)) return; 293 | if (schema.properties && schema.properties[property] !== undefined) { 294 | return; 295 | } 296 | if (schema.additionalProperties === false) { 297 | result.addError({ 298 | name: 'additionalProperties', 299 | argument: property, 300 | message: "is not allowed to have the additional property " + JSON.stringify(property), 301 | }); 302 | } else { 303 | var additionalProperties = schema.additionalProperties || {}; 304 | 305 | if (typeof options.preValidateProperty == 'function') { 306 | options.preValidateProperty(instance, property, additionalProperties, options, ctx); 307 | } 308 | 309 | var res = this.validateSchema(instance[property], additionalProperties, options, ctx.makeChild(additionalProperties, property)); 310 | if(res.instance !== result.instance[property]) result.instance[property] = res.instance; 311 | result.importErrors(res); 312 | } 313 | } 314 | 315 | /** 316 | * Validates patternProperties 317 | * @param instance 318 | * @param schema 319 | * @param options 320 | * @param ctx 321 | * @return {String|null|ValidatorResult} 322 | */ 323 | validators.patternProperties = function validatePatternProperties (instance, schema, options, ctx) { 324 | if(!this.types.object(instance)) return; 325 | var result = new ValidatorResult(instance, schema, options, ctx); 326 | var patternProperties = schema.patternProperties || {}; 327 | 328 | for (var property in instance) { 329 | var test = true; 330 | for (var pattern in patternProperties) { 331 | var subschema = patternProperties[pattern]; 332 | if(subschema===undefined){ 333 | continue; 334 | }else if(subschema===null){ 335 | throw new SchemaError('Unexpected null, expected schema in "patternProperties"'); 336 | } 337 | try { 338 | var regexp = new RegExp(pattern, 'u'); 339 | } catch(_e) { 340 | // In the event the stricter handling causes an error, fall back on the forgiving handling 341 | // DEPRECATED 342 | regexp = new RegExp(pattern); 343 | } 344 | if (!regexp.test(property)) { 345 | continue; 346 | } 347 | test = false; 348 | 349 | if (typeof options.preValidateProperty == 'function') { 350 | options.preValidateProperty(instance, property, subschema, options, ctx); 351 | } 352 | 353 | var res = this.validateSchema(instance[property], subschema, options, ctx.makeChild(subschema, property)); 354 | if(res.instance !== result.instance[property]) result.instance[property] = res.instance; 355 | result.importErrors(res); 356 | } 357 | if (test) { 358 | testAdditionalProperty.call(this, instance, schema, options, ctx, property, result); 359 | } 360 | } 361 | 362 | return result; 363 | }; 364 | 365 | /** 366 | * Validates additionalProperties 367 | * @param instance 368 | * @param schema 369 | * @param options 370 | * @param ctx 371 | * @return {String|null|ValidatorResult} 372 | */ 373 | validators.additionalProperties = function validateAdditionalProperties (instance, schema, options, ctx) { 374 | if(!this.types.object(instance)) return; 375 | // if patternProperties is defined then we'll test when that one is called instead 376 | if (schema.patternProperties) { 377 | return null; 378 | } 379 | var result = new ValidatorResult(instance, schema, options, ctx); 380 | for (var property in instance) { 381 | testAdditionalProperty.call(this, instance, schema, options, ctx, property, result); 382 | } 383 | return result; 384 | }; 385 | 386 | /** 387 | * Validates whether the instance value is at least of a certain length, when the instance value is a string. 388 | * @param instance 389 | * @param schema 390 | * @return {String|null} 391 | */ 392 | validators.minProperties = function validateMinProperties (instance, schema, options, ctx) { 393 | if (!this.types.object(instance)) return; 394 | var result = new ValidatorResult(instance, schema, options, ctx); 395 | var keys = Object.keys(instance); 396 | if (!(keys.length >= schema.minProperties)) { 397 | result.addError({ 398 | name: 'minProperties', 399 | argument: schema.minProperties, 400 | message: "does not meet minimum property length of " + schema.minProperties, 401 | }); 402 | } 403 | return result; 404 | }; 405 | 406 | /** 407 | * Validates whether the instance value is at most of a certain length, when the instance value is a string. 408 | * @param instance 409 | * @param schema 410 | * @return {String|null} 411 | */ 412 | validators.maxProperties = function validateMaxProperties (instance, schema, options, ctx) { 413 | if (!this.types.object(instance)) return; 414 | var result = new ValidatorResult(instance, schema, options, ctx); 415 | var keys = Object.keys(instance); 416 | if (!(keys.length <= schema.maxProperties)) { 417 | result.addError({ 418 | name: 'maxProperties', 419 | argument: schema.maxProperties, 420 | message: "does not meet maximum property length of " + schema.maxProperties, 421 | }); 422 | } 423 | return result; 424 | }; 425 | 426 | /** 427 | * Validates items when instance is an array 428 | * @param instance 429 | * @param schema 430 | * @param options 431 | * @param ctx 432 | * @return {String|null|ValidatorResult} 433 | */ 434 | validators.items = function validateItems (instance, schema, options, ctx) { 435 | var self = this; 436 | if (!this.types.array(instance)) return; 437 | if (schema.items===undefined) return; 438 | var result = new ValidatorResult(instance, schema, options, ctx); 439 | instance.every(function (value, i) { 440 | if(Array.isArray(schema.items)){ 441 | var items = schema.items[i]===undefined ? schema.additionalItems : schema.items[i]; 442 | }else{ 443 | var items = schema.items; 444 | } 445 | if (items === undefined) { 446 | return true; 447 | } 448 | if (items === false) { 449 | result.addError({ 450 | name: 'items', 451 | message: "additionalItems not permitted", 452 | }); 453 | return false; 454 | } 455 | var res = self.validateSchema(value, items, options, ctx.makeChild(items, i)); 456 | if(res.instance !== result.instance[i]) result.instance[i] = res.instance; 457 | result.importErrors(res); 458 | return true; 459 | }); 460 | return result; 461 | }; 462 | 463 | /** 464 | * Validates the "contains" keyword 465 | * @param instance 466 | * @param schema 467 | * @param options 468 | * @param ctx 469 | * @return {String|null|ValidatorResult} 470 | */ 471 | validators.contains = function validateContains (instance, schema, options, ctx) { 472 | var self = this; 473 | if (!this.types.array(instance)) return; 474 | if (schema.contains===undefined) return; 475 | if (!helpers.isSchema(schema.contains)) throw new Error('Expected "contains" keyword to be a schema'); 476 | var result = new ValidatorResult(instance, schema, options, ctx); 477 | var count = instance.some(function (value, i) { 478 | var res = self.validateSchema(value, schema.contains, options, ctx.makeChild(schema.contains, i)); 479 | return res.errors.length===0; 480 | }); 481 | if(count===false){ 482 | result.addError({ 483 | name: 'contains', 484 | argument: schema.contains, 485 | message: "must contain an item matching given schema", 486 | }); 487 | } 488 | return result; 489 | }; 490 | 491 | /** 492 | * Validates minimum and exclusiveMinimum when the type of the instance value is a number. 493 | * @param instance 494 | * @param schema 495 | * @return {String|null} 496 | */ 497 | validators.minimum = function validateMinimum (instance, schema, options, ctx) { 498 | if (!this.types.number(instance)) return; 499 | var result = new ValidatorResult(instance, schema, options, ctx); 500 | if (schema.exclusiveMinimum && schema.exclusiveMinimum === true) { 501 | if(!(instance > schema.minimum)){ 502 | result.addError({ 503 | name: 'minimum', 504 | argument: schema.minimum, 505 | message: "must be greater than " + schema.minimum, 506 | }); 507 | } 508 | } else { 509 | if(!(instance >= schema.minimum)){ 510 | result.addError({ 511 | name: 'minimum', 512 | argument: schema.minimum, 513 | message: "must be greater than or equal to " + schema.minimum, 514 | }); 515 | } 516 | } 517 | return result; 518 | }; 519 | 520 | /** 521 | * Validates maximum and exclusiveMaximum when the type of the instance value is a number. 522 | * @param instance 523 | * @param schema 524 | * @return {String|null} 525 | */ 526 | validators.maximum = function validateMaximum (instance, schema, options, ctx) { 527 | if (!this.types.number(instance)) return; 528 | var result = new ValidatorResult(instance, schema, options, ctx); 529 | if (schema.exclusiveMaximum && schema.exclusiveMaximum === true) { 530 | if(!(instance < schema.maximum)){ 531 | result.addError({ 532 | name: 'maximum', 533 | argument: schema.maximum, 534 | message: "must be less than " + schema.maximum, 535 | }); 536 | } 537 | } else { 538 | if(!(instance <= schema.maximum)){ 539 | result.addError({ 540 | name: 'maximum', 541 | argument: schema.maximum, 542 | message: "must be less than or equal to " + schema.maximum, 543 | }); 544 | } 545 | } 546 | return result; 547 | }; 548 | 549 | /** 550 | * Validates the number form of exclusiveMinimum when the type of the instance value is a number. 551 | * @param instance 552 | * @param schema 553 | * @return {String|null} 554 | */ 555 | validators.exclusiveMinimum = function validateExclusiveMinimum (instance, schema, options, ctx) { 556 | // Support the boolean form of exclusiveMinimum, which is handled by the "minimum" keyword. 557 | if(typeof schema.exclusiveMinimum === 'boolean') return; 558 | if (!this.types.number(instance)) return; 559 | var result = new ValidatorResult(instance, schema, options, ctx); 560 | var valid = instance > schema.exclusiveMinimum; 561 | if (!valid) { 562 | result.addError({ 563 | name: 'exclusiveMinimum', 564 | argument: schema.exclusiveMinimum, 565 | message: "must be strictly greater than " + schema.exclusiveMinimum, 566 | }); 567 | } 568 | return result; 569 | }; 570 | 571 | /** 572 | * Validates the number form of exclusiveMaximum when the type of the instance value is a number. 573 | * @param instance 574 | * @param schema 575 | * @return {String|null} 576 | */ 577 | validators.exclusiveMaximum = function validateExclusiveMaximum (instance, schema, options, ctx) { 578 | // Support the boolean form of exclusiveMaximum, which is handled by the "maximum" keyword. 579 | if(typeof schema.exclusiveMaximum === 'boolean') return; 580 | if (!this.types.number(instance)) return; 581 | var result = new ValidatorResult(instance, schema, options, ctx); 582 | var valid = instance < schema.exclusiveMaximum; 583 | if (!valid) { 584 | result.addError({ 585 | name: 'exclusiveMaximum', 586 | argument: schema.exclusiveMaximum, 587 | message: "must be strictly less than " + schema.exclusiveMaximum, 588 | }); 589 | } 590 | return result; 591 | }; 592 | 593 | /** 594 | * Perform validation for multipleOf and divisibleBy, which are essentially the same. 595 | * @param instance 596 | * @param schema 597 | * @param validationType 598 | * @param errorMessage 599 | * @returns {String|null} 600 | */ 601 | var validateMultipleOfOrDivisbleBy = function validateMultipleOfOrDivisbleBy (instance, schema, options, ctx, validationType, errorMessage) { 602 | if (!this.types.number(instance)) return; 603 | 604 | var validationArgument = schema[validationType]; 605 | if (validationArgument == 0) { 606 | throw new SchemaError(validationType + " cannot be zero"); 607 | } 608 | 609 | var result = new ValidatorResult(instance, schema, options, ctx); 610 | 611 | var instanceDecimals = helpers.getDecimalPlaces(instance); 612 | var divisorDecimals = helpers.getDecimalPlaces(validationArgument); 613 | 614 | var maxDecimals = Math.max(instanceDecimals , divisorDecimals); 615 | var multiplier = Math.pow(10, maxDecimals); 616 | 617 | if (Math.round(instance * multiplier) % Math.round(validationArgument * multiplier) !== 0) { 618 | result.addError({ 619 | name: validationType, 620 | argument: validationArgument, 621 | message: errorMessage + JSON.stringify(validationArgument), 622 | }); 623 | } 624 | 625 | return result; 626 | }; 627 | 628 | /** 629 | * Validates divisibleBy when the type of the instance value is a number. 630 | * @param instance 631 | * @param schema 632 | * @return {String|null} 633 | */ 634 | validators.multipleOf = function validateMultipleOf (instance, schema, options, ctx) { 635 | return validateMultipleOfOrDivisbleBy.call(this, instance, schema, options, ctx, "multipleOf", "is not a multiple of (divisible by) "); 636 | }; 637 | 638 | /** 639 | * Validates multipleOf when the type of the instance value is a number. 640 | * @param instance 641 | * @param schema 642 | * @return {String|null} 643 | */ 644 | validators.divisibleBy = function validateDivisibleBy (instance, schema, options, ctx) { 645 | return validateMultipleOfOrDivisbleBy.call(this, instance, schema, options, ctx, "divisibleBy", "is not divisible by (multiple of) "); 646 | }; 647 | 648 | /** 649 | * Validates whether the instance value is present. 650 | * @param instance 651 | * @param schema 652 | * @return {String|null} 653 | */ 654 | validators.required = function validateRequired (instance, schema, options, ctx) { 655 | var result = new ValidatorResult(instance, schema, options, ctx); 656 | if (instance === undefined && schema.required === true) { 657 | // A boolean form is implemented for reverse-compatibility with schemas written against older drafts 658 | result.addError({ 659 | name: 'required', 660 | message: "is required", 661 | }); 662 | } else if (this.types.object(instance) && Array.isArray(schema.required)) { 663 | schema.required.forEach(function(n){ 664 | if(getEnumerableProperty(instance, n)===undefined){ 665 | result.addError({ 666 | name: 'required', 667 | argument: n, 668 | message: "requires property " + JSON.stringify(n), 669 | }); 670 | } 671 | }); 672 | } 673 | return result; 674 | }; 675 | 676 | /** 677 | * Validates whether the instance value matches the regular expression, when the instance value is a string. 678 | * @param instance 679 | * @param schema 680 | * @return {String|null} 681 | */ 682 | validators.pattern = function validatePattern (instance, schema, options, ctx) { 683 | if (!this.types.string(instance)) return; 684 | var result = new ValidatorResult(instance, schema, options, ctx); 685 | var pattern = schema.pattern; 686 | try { 687 | var regexp = new RegExp(pattern, 'u'); 688 | } catch(_e) { 689 | // In the event the stricter handling causes an error, fall back on the forgiving handling 690 | // DEPRECATED 691 | regexp = new RegExp(pattern); 692 | } 693 | if (!instance.match(regexp)) { 694 | result.addError({ 695 | name: 'pattern', 696 | argument: schema.pattern, 697 | message: "does not match pattern " + JSON.stringify(schema.pattern.toString()), 698 | }); 699 | } 700 | return result; 701 | }; 702 | 703 | /** 704 | * Validates whether the instance value is of a certain defined format or a custom 705 | * format. 706 | * The following formats are supported for string types: 707 | * - date-time 708 | * - date 709 | * - time 710 | * - ip-address 711 | * - ipv6 712 | * - uri 713 | * - color 714 | * - host-name 715 | * - alpha 716 | * - alpha-numeric 717 | * - utc-millisec 718 | * @param instance 719 | * @param schema 720 | * @param [options] 721 | * @param [ctx] 722 | * @return {String|null} 723 | */ 724 | validators.format = function validateFormat (instance, schema, options, ctx) { 725 | if (instance===undefined) return; 726 | var result = new ValidatorResult(instance, schema, options, ctx); 727 | if (!result.disableFormat && !helpers.isFormat(instance, schema.format, this)) { 728 | result.addError({ 729 | name: 'format', 730 | argument: schema.format, 731 | message: "does not conform to the " + JSON.stringify(schema.format) + " format", 732 | }); 733 | } 734 | return result; 735 | }; 736 | 737 | /** 738 | * Validates whether the instance value is at least of a certain length, when the instance value is a string. 739 | * @param instance 740 | * @param schema 741 | * @return {String|null} 742 | */ 743 | validators.minLength = function validateMinLength (instance, schema, options, ctx) { 744 | if (!this.types.string(instance)) return; 745 | var result = new ValidatorResult(instance, schema, options, ctx); 746 | var hsp = instance.match(/[\uDC00-\uDFFF]/g); 747 | var length = instance.length - (hsp ? hsp.length : 0); 748 | if (!(length >= schema.minLength)) { 749 | result.addError({ 750 | name: 'minLength', 751 | argument: schema.minLength, 752 | message: "does not meet minimum length of " + schema.minLength, 753 | }); 754 | } 755 | return result; 756 | }; 757 | 758 | /** 759 | * Validates whether the instance value is at most of a certain length, when the instance value is a string. 760 | * @param instance 761 | * @param schema 762 | * @return {String|null} 763 | */ 764 | validators.maxLength = function validateMaxLength (instance, schema, options, ctx) { 765 | if (!this.types.string(instance)) return; 766 | var result = new ValidatorResult(instance, schema, options, ctx); 767 | // TODO if this was already computed in "minLength", use that value instead of re-computing 768 | var hsp = instance.match(/[\uDC00-\uDFFF]/g); 769 | var length = instance.length - (hsp ? hsp.length : 0); 770 | if (!(length <= schema.maxLength)) { 771 | result.addError({ 772 | name: 'maxLength', 773 | argument: schema.maxLength, 774 | message: "does not meet maximum length of " + schema.maxLength, 775 | }); 776 | } 777 | return result; 778 | }; 779 | 780 | /** 781 | * Validates whether instance contains at least a minimum number of items, when the instance is an Array. 782 | * @param instance 783 | * @param schema 784 | * @return {String|null} 785 | */ 786 | validators.minItems = function validateMinItems (instance, schema, options, ctx) { 787 | if (!this.types.array(instance)) return; 788 | var result = new ValidatorResult(instance, schema, options, ctx); 789 | if (!(instance.length >= schema.minItems)) { 790 | result.addError({ 791 | name: 'minItems', 792 | argument: schema.minItems, 793 | message: "does not meet minimum length of " + schema.minItems, 794 | }); 795 | } 796 | return result; 797 | }; 798 | 799 | /** 800 | * Validates whether instance contains no more than a maximum number of items, when the instance is an Array. 801 | * @param instance 802 | * @param schema 803 | * @return {String|null} 804 | */ 805 | validators.maxItems = function validateMaxItems (instance, schema, options, ctx) { 806 | if (!this.types.array(instance)) return; 807 | var result = new ValidatorResult(instance, schema, options, ctx); 808 | if (!(instance.length <= schema.maxItems)) { 809 | result.addError({ 810 | name: 'maxItems', 811 | argument: schema.maxItems, 812 | message: "does not meet maximum length of " + schema.maxItems, 813 | }); 814 | } 815 | return result; 816 | }; 817 | 818 | /** 819 | * Deep compares arrays for duplicates 820 | * @param v 821 | * @param i 822 | * @param a 823 | * @private 824 | * @return {boolean} 825 | */ 826 | function testArrays (v, i, a) { 827 | var j, len = a.length; 828 | for (j = i + 1, len; j < len; j++) { 829 | if (helpers.deepCompareStrict(v, a[j])) { 830 | return false; 831 | } 832 | } 833 | return true; 834 | } 835 | 836 | /** 837 | * Validates whether there are no duplicates, when the instance is an Array. 838 | * @param instance 839 | * @return {String|null} 840 | */ 841 | validators.uniqueItems = function validateUniqueItems (instance, schema, options, ctx) { 842 | if (schema.uniqueItems!==true) return; 843 | if (!this.types.array(instance)) return; 844 | var result = new ValidatorResult(instance, schema, options, ctx); 845 | if (!instance.every(testArrays)) { 846 | result.addError({ 847 | name: 'uniqueItems', 848 | message: "contains duplicate item", 849 | }); 850 | } 851 | return result; 852 | }; 853 | 854 | /** 855 | * Validate for the presence of dependency properties, if the instance is an object. 856 | * @param instance 857 | * @param schema 858 | * @param options 859 | * @param ctx 860 | * @return {null|ValidatorResult} 861 | */ 862 | validators.dependencies = function validateDependencies (instance, schema, options, ctx) { 863 | if (!this.types.object(instance)) return; 864 | var result = new ValidatorResult(instance, schema, options, ctx); 865 | for (var property in schema.dependencies) { 866 | if (instance[property] === undefined) { 867 | continue; 868 | } 869 | var dep = schema.dependencies[property]; 870 | var childContext = ctx.makeChild(dep, property); 871 | if (typeof dep == 'string') { 872 | dep = [dep]; 873 | } 874 | if (Array.isArray(dep)) { 875 | dep.forEach(function (prop) { 876 | if (instance[prop] === undefined) { 877 | result.addError({ 878 | // FIXME there's two different "dependencies" errors here with slightly different outputs 879 | // Can we make these the same? Or should we create different error types? 880 | name: 'dependencies', 881 | argument: childContext.propertyPath, 882 | message: "property " + prop + " not found, required by " + childContext.propertyPath, 883 | }); 884 | } 885 | }); 886 | } else { 887 | var res = this.validateSchema(instance, dep, options, childContext); 888 | if(result.instance !== res.instance) result.instance = res.instance; 889 | if (res && res.errors.length) { 890 | result.addError({ 891 | name: 'dependencies', 892 | argument: childContext.propertyPath, 893 | message: "does not meet dependency required by " + childContext.propertyPath, 894 | }); 895 | result.importErrors(res); 896 | } 897 | } 898 | } 899 | return result; 900 | }; 901 | 902 | /** 903 | * Validates whether the instance value is one of the enumerated values. 904 | * 905 | * @param instance 906 | * @param schema 907 | * @return {ValidatorResult|null} 908 | */ 909 | validators['enum'] = function validateEnum (instance, schema, options, ctx) { 910 | if (instance === undefined) { 911 | return null; 912 | } 913 | if (!Array.isArray(schema['enum'])) { 914 | throw new SchemaError("enum expects an array", schema); 915 | } 916 | var result = new ValidatorResult(instance, schema, options, ctx); 917 | if (!schema['enum'].some(helpers.deepCompareStrict.bind(null, instance))) { 918 | result.addError({ 919 | name: 'enum', 920 | argument: schema['enum'], 921 | message: "is not one of enum values: " + schema['enum'].map(String).join(','), 922 | }); 923 | } 924 | return result; 925 | }; 926 | 927 | /** 928 | * Validates whether the instance exactly matches a given value 929 | * 930 | * @param instance 931 | * @param schema 932 | * @return {ValidatorResult|null} 933 | */ 934 | validators['const'] = function validateEnum (instance, schema, options, ctx) { 935 | if (instance === undefined) { 936 | return null; 937 | } 938 | var result = new ValidatorResult(instance, schema, options, ctx); 939 | if (!helpers.deepCompareStrict(schema['const'], instance)) { 940 | result.addError({ 941 | name: 'const', 942 | argument: schema['const'], 943 | message: "does not exactly match expected constant: " + schema['const'], 944 | }); 945 | } 946 | return result; 947 | }; 948 | 949 | /** 950 | * Validates whether the instance is of a prohibited type. 951 | * @param instance 952 | * @param schema 953 | * @param options 954 | * @param ctx 955 | * @return {null|ValidatorResult} 956 | */ 957 | validators.not = validators.disallow = function validateNot (instance, schema, options, ctx) { 958 | var self = this; 959 | if(instance===undefined) return null; 960 | var result = new ValidatorResult(instance, schema, options, ctx); 961 | var notTypes = schema.not || schema.disallow; 962 | if(!notTypes) return null; 963 | if(!Array.isArray(notTypes)) notTypes=[notTypes]; 964 | notTypes.forEach(function (type) { 965 | if (self.testType(instance, schema, options, ctx, type)) { 966 | var id = type && (type.$id || type.id); 967 | var schemaId = id || type; 968 | result.addError({ 969 | name: 'not', 970 | argument: schemaId, 971 | message: "is of prohibited type " + schemaId, 972 | }); 973 | } 974 | }); 975 | return result; 976 | }; 977 | 978 | module.exports = attribute; 979 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = exports.ValidationError = function ValidationError (message, instance, schema, path, name, argument) { 4 | if(Array.isArray(path)){ 5 | this.path = path; 6 | this.property = path.reduce(function(sum, item){ 7 | return sum + makeSuffix(item); 8 | }, 'instance'); 9 | }else if(path !== undefined){ 10 | this.property = path; 11 | } 12 | if (message) { 13 | this.message = message; 14 | } 15 | if (schema) { 16 | var id = schema.$id || schema.id; 17 | this.schema = id || schema; 18 | } 19 | if (instance !== undefined) { 20 | this.instance = instance; 21 | } 22 | this.name = name; 23 | this.argument = argument; 24 | this.stack = this.toString(); 25 | }; 26 | 27 | ValidationError.prototype.toString = function toString() { 28 | return this.property + ' ' + this.message; 29 | }; 30 | 31 | var ValidatorResult = exports.ValidatorResult = function ValidatorResult(instance, schema, options, ctx) { 32 | this.instance = instance; 33 | this.schema = schema; 34 | this.options = options; 35 | this.path = ctx.path; 36 | this.propertyPath = ctx.propertyPath; 37 | this.errors = []; 38 | this.throwError = options && options.throwError; 39 | this.throwFirst = options && options.throwFirst; 40 | this.throwAll = options && options.throwAll; 41 | this.disableFormat = options && options.disableFormat === true; 42 | }; 43 | 44 | ValidatorResult.prototype.addError = function addError(detail) { 45 | var err; 46 | if (typeof detail == 'string') { 47 | err = new ValidationError(detail, this.instance, this.schema, this.path); 48 | } else { 49 | if (!detail) throw new Error('Missing error detail'); 50 | if (!detail.message) throw new Error('Missing error message'); 51 | if (!detail.name) throw new Error('Missing validator type'); 52 | err = new ValidationError(detail.message, this.instance, this.schema, this.path, detail.name, detail.argument); 53 | } 54 | 55 | this.errors.push(err); 56 | if (this.throwFirst) { 57 | throw new ValidatorResultError(this); 58 | }else if(this.throwError){ 59 | throw err; 60 | } 61 | return err; 62 | }; 63 | 64 | ValidatorResult.prototype.importErrors = function importErrors(res) { 65 | if (typeof res == 'string' || (res && res.validatorType)) { 66 | this.addError(res); 67 | } else if (res && res.errors) { 68 | this.errors = this.errors.concat(res.errors); 69 | } 70 | }; 71 | 72 | function stringizer (v,i){ 73 | return i+': '+v.toString()+'\n'; 74 | } 75 | ValidatorResult.prototype.toString = function toString(res) { 76 | return this.errors.map(stringizer).join(''); 77 | }; 78 | 79 | Object.defineProperty(ValidatorResult.prototype, "valid", { get: function() { 80 | return !this.errors.length; 81 | } }); 82 | 83 | module.exports.ValidatorResultError = ValidatorResultError; 84 | function ValidatorResultError(result) { 85 | if(typeof Error.captureStackTrace === 'function'){ 86 | Error.captureStackTrace(this, ValidatorResultError); 87 | } 88 | this.instance = result.instance; 89 | this.schema = result.schema; 90 | this.options = result.options; 91 | this.errors = result.errors; 92 | } 93 | ValidatorResultError.prototype = new Error(); 94 | ValidatorResultError.prototype.constructor = ValidatorResultError; 95 | ValidatorResultError.prototype.name = "Validation Error"; 96 | 97 | /** 98 | * Describes a problem with a Schema which prevents validation of an instance 99 | * @name SchemaError 100 | * @constructor 101 | */ 102 | var SchemaError = exports.SchemaError = function SchemaError (msg, schema) { 103 | this.message = msg; 104 | this.schema = schema; 105 | Error.call(this, msg); 106 | if(typeof Error.captureStackTrace === 'function'){ 107 | Error.captureStackTrace(this, SchemaError); 108 | } 109 | }; 110 | SchemaError.prototype = Object.create(Error.prototype, 111 | { 112 | constructor: {value: SchemaError, enumerable: false}, 113 | name: {value: 'SchemaError', enumerable: false}, 114 | }); 115 | 116 | var SchemaContext = exports.SchemaContext = function SchemaContext (schema, options, path, base, schemas) { 117 | this.schema = schema; 118 | this.options = options; 119 | if(Array.isArray(path)){ 120 | this.path = path; 121 | this.propertyPath = path.reduce(function(sum, item){ 122 | return sum + makeSuffix(item); 123 | }, 'instance'); 124 | }else{ 125 | this.propertyPath = path; 126 | } 127 | this.base = base; 128 | this.schemas = schemas; 129 | }; 130 | 131 | SchemaContext.prototype.resolve = function resolve (target) { 132 | return (() => resolveUrl(this.base,target))(); 133 | }; 134 | 135 | SchemaContext.prototype.makeChild = function makeChild(schema, propertyName){ 136 | var path = (propertyName===undefined) ? this.path : this.path.concat([propertyName]); 137 | var id = schema.$id || schema.id; 138 | let base = (() => resolveUrl(this.base,id||''))(); 139 | var ctx = new SchemaContext(schema, this.options, path, base, Object.create(this.schemas)); 140 | if(id && !ctx.schemas[base]){ 141 | ctx.schemas[base] = schema; 142 | } 143 | return ctx; 144 | }; 145 | 146 | var FORMAT_REGEXPS = exports.FORMAT_REGEXPS = { 147 | // 7.3.1. Dates, Times, and Duration 148 | 'date-time': /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-(3[01]|0[1-9]|[12][0-9])[tT ](2[0-4]|[01][0-9]):([0-5][0-9]):(60|[0-5][0-9])(\.\d+)?([zZ]|[+-]([0-5][0-9]):(60|[0-5][0-9]))$/, 149 | 'date': /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-(3[01]|0[1-9]|[12][0-9])$/, 150 | 'time': /^(2[0-4]|[01][0-9]):([0-5][0-9]):(60|[0-5][0-9])$/, 151 | 'duration': /P(T\d+(H(\d+M(\d+S)?)?|M(\d+S)?|S)|\d+(D|M(\d+D)?|Y(\d+M(\d+D)?)?)(T\d+(H(\d+M(\d+S)?)?|M(\d+S)?|S))?|\d+W)/i, 152 | 153 | // 7.3.2. Email Addresses 154 | // TODO: fix the email production 155 | 'email': /^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/, 156 | 'idn-email': /^("(?:[!#-\[\]-\u{10FFFF}]|\\[\t -\u{10FFFF}])*"|[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}](?:\.?[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}])*)@([!#-'*+\-/-9=?A-Z\^-\u{10FFFF}](?:\.?[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}])*|\[[!-Z\^-\u{10FFFF}]*\])$/u, 157 | 158 | // 7.3.3. Hostnames 159 | 160 | // 7.3.4. IP Addresses 161 | 'ip-address': /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, 162 | // FIXME whitespace is invalid 163 | 'ipv6': /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-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-Fa-f]{1,4}:){5}(((:[0-9A-Fa-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-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-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-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-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-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-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-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-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-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-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*$/, 164 | 165 | // 7.3.5. Resource Identifiers 166 | // TODO: A more accurate regular expression for "uri" goes: 167 | // [A-Za-z][+\-.0-9A-Za-z]*:((/(/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?)?)?#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|(/(/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~])|/?%[0-9A-Fa-f]{2}|[!$&-.0-;=?-Z_a-z~])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*(#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*)?|/(/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+(:\d*)?|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?:\d*|\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)?)? 168 | 'uri': /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/, 169 | 'uri-reference': /^(((([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|([A-Za-z][+\-.0-9A-Za-z]*:?)?)|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|(\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?)?))#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|(([A-Za-z][+\-.0-9A-Za-z]*)?%[0-9A-Fa-f]{2}|[!$&-.0-9;=@_~]|[A-Za-z][+\-.0-9A-Za-z]*[!$&-*,;=@_~])(%[0-9A-Fa-f]{2}|[!$&-.0-9;=@-Z_a-z~])*((([/?](%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*)?#|[/?])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*)?|([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+(:\d*)?|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?:\d*|\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)?|[A-Za-z][+\-.0-9A-Za-z]*:?)?$/, 170 | 'iri': /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/, 171 | 'iri-reference': /^(((([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~-\u{10FFFF}]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|([A-Za-z][+\-.0-9A-Za-z]*:?)?)|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~-\u{10FFFF}])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|(\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?)?))#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|(([A-Za-z][+\-.0-9A-Za-z]*)?%[0-9A-Fa-f]{2}|[!$&-.0-9;=@_~-\u{10FFFF}]|[A-Za-z][+\-.0-9A-Za-z]*[!$&-*,;=@_~-\u{10FFFF}])(%[0-9A-Fa-f]{2}|[!$&-.0-9;=@-Z_a-z~-\u{10FFFF}])*((([/?](%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*)?#|[/?])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*)?|([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~-\u{10FFFF}]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~-\u{10FFFF}])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+(:\d*)?|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?:\d*|\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)?|[A-Za-z][+\-.0-9A-Za-z]*:?)?$/u, 172 | 'uuid': /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i, 173 | 174 | // 7.3.6. uri-template 175 | 'uri-template': /(%[0-9a-f]{2}|[!#$&(-;=?@\[\]_a-z~]|\{[!#&+,./;=?@|]?(%[0-9a-f]{2}|[0-9_a-z])(\.?(%[0-9a-f]{2}|[0-9_a-z]))*(:[1-9]\d{0,3}|\*)?(,(%[0-9a-f]{2}|[0-9_a-z])(\.?(%[0-9a-f]{2}|[0-9_a-z]))*(:[1-9]\d{0,3}|\*)?)*\})*/iu, 176 | 177 | // 7.3.7. JSON Pointers 178 | 'json-pointer': /^(\/([\x00-\x2e0-@\[-}\x7f]|~[01])*)*$/iu, 179 | 'relative-json-pointer': /^\d+(#|(\/([\x00-\x2e0-@\[-}\x7f]|~[01])*)*)$/iu, 180 | 181 | // hostname regex from: http://stackoverflow.com/a/1420225/5628 182 | 'hostname': /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/, 183 | 'host-name': /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/, 184 | 185 | 'utc-millisec': function (input) { 186 | return (typeof input === 'string') && parseFloat(input) === parseInt(input, 10) && !isNaN(input); 187 | }, 188 | 189 | // 7.3.8. regex 190 | 'regex': function (input) { 191 | var result = true; 192 | try { 193 | new RegExp(input); 194 | } catch (e) { 195 | result = false; 196 | } 197 | return result; 198 | }, 199 | 200 | // Other definitions 201 | // "style" was removed from JSON Schema in draft-4 and is deprecated 202 | 'style': /[\r\n\t ]*[^\r\n\t ][^:]*:[\r\n\t ]*[^\r\n\t ;]*[\r\n\t ]*;?/, 203 | // "color" was removed from JSON Schema in draft-4 and is deprecated 204 | 'color': /^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/, 205 | 'phone': /^\+(?:[0-9] ?){6,14}[0-9]$/, 206 | 'alpha': /^[a-zA-Z]+$/, 207 | 'alphanumeric': /^[a-zA-Z0-9]+$/, 208 | }; 209 | 210 | FORMAT_REGEXPS.regexp = FORMAT_REGEXPS.regex; 211 | FORMAT_REGEXPS.pattern = FORMAT_REGEXPS.regex; 212 | FORMAT_REGEXPS.ipv4 = FORMAT_REGEXPS['ip-address']; 213 | 214 | exports.isFormat = function isFormat (input, format, validator) { 215 | if (typeof input === 'string' && FORMAT_REGEXPS[format] !== undefined) { 216 | if (FORMAT_REGEXPS[format] instanceof RegExp) { 217 | return FORMAT_REGEXPS[format].test(input); 218 | } 219 | if (typeof FORMAT_REGEXPS[format] === 'function') { 220 | return FORMAT_REGEXPS[format](input); 221 | } 222 | } else if (validator && validator.customFormats && 223 | typeof validator.customFormats[format] === 'function') { 224 | return validator.customFormats[format](input); 225 | } 226 | return true; 227 | }; 228 | 229 | var makeSuffix = exports.makeSuffix = function makeSuffix (key) { 230 | key = key.toString(); 231 | // This function could be capable of outputting valid a ECMAScript string, but the 232 | // resulting code for testing which form to use would be tens of thousands of characters long 233 | // That means this will use the name form for some illegal forms 234 | if (!key.match(/[.\s\[\]]/) && !key.match(/^[\d]/)) { 235 | return '.' + key; 236 | } 237 | if (key.match(/^\d+$/)) { 238 | return '[' + key + ']'; 239 | } 240 | return '[' + JSON.stringify(key) + ']'; 241 | }; 242 | 243 | exports.deepCompareStrict = function deepCompareStrict (a, b) { 244 | if (typeof a !== typeof b) { 245 | return false; 246 | } 247 | if (Array.isArray(a)) { 248 | if (!Array.isArray(b)) { 249 | return false; 250 | } 251 | if (a.length !== b.length) { 252 | return false; 253 | } 254 | return a.every(function (v, i) { 255 | return deepCompareStrict(a[i], b[i]); 256 | }); 257 | } 258 | if (typeof a === 'object') { 259 | if (!a || !b) { 260 | return a === b; 261 | } 262 | var aKeys = Object.keys(a); 263 | var bKeys = Object.keys(b); 264 | if (aKeys.length !== bKeys.length) { 265 | return false; 266 | } 267 | return aKeys.every(function (v) { 268 | return deepCompareStrict(a[v], b[v]); 269 | }); 270 | } 271 | return a === b; 272 | }; 273 | 274 | function deepMerger (target, dst, e, i) { 275 | if (typeof e === 'object') { 276 | dst[i] = deepMerge(target[i], e); 277 | } else { 278 | if (target.indexOf(e) === -1) { 279 | dst.push(e); 280 | } 281 | } 282 | } 283 | 284 | function copyist (src, dst, key) { 285 | dst[key] = src[key]; 286 | } 287 | 288 | function copyistWithDeepMerge (target, src, dst, key) { 289 | if (typeof src[key] !== 'object' || !src[key]) { 290 | dst[key] = src[key]; 291 | } 292 | else { 293 | if (!target[key]) { 294 | dst[key] = src[key]; 295 | } else { 296 | dst[key] = deepMerge(target[key], src[key]); 297 | } 298 | } 299 | } 300 | 301 | function deepMerge (target, src) { 302 | var array = Array.isArray(src); 303 | var dst = array && [] || {}; 304 | 305 | if (array) { 306 | target = target || []; 307 | dst = dst.concat(target); 308 | src.forEach(deepMerger.bind(null, target, dst)); 309 | } else { 310 | if (target && typeof target === 'object') { 311 | Object.keys(target).forEach(copyist.bind(null, target, dst)); 312 | } 313 | Object.keys(src).forEach(copyistWithDeepMerge.bind(null, target, src, dst)); 314 | } 315 | 316 | return dst; 317 | } 318 | 319 | module.exports.deepMerge = deepMerge; 320 | 321 | /** 322 | * Validates instance against the provided schema 323 | * Implements URI+JSON Pointer encoding, e.g. "%7e"="~0"=>"~", "~1"="%2f"=>"/" 324 | * @param o 325 | * @param s The path to walk o along 326 | * @return any 327 | */ 328 | exports.objectGetPath = function objectGetPath(o, s) { 329 | var parts = s.split('/').slice(1); 330 | var k; 331 | while (typeof (k=parts.shift()) == 'string') { 332 | var n = decodeURIComponent(k.replace(/~0/,'~').replace(/~1/g,'/')); 333 | if (!(n in o)) return; 334 | o = o[n]; 335 | } 336 | return o; 337 | }; 338 | 339 | function pathEncoder (v) { 340 | return '/'+encodeURIComponent(v).replace(/~/g,'%7E'); 341 | } 342 | /** 343 | * Accept an Array of property names and return a JSON Pointer URI fragment 344 | * @param Array a 345 | * @return {String} 346 | */ 347 | exports.encodePath = function encodePointer(a){ 348 | // ~ must be encoded explicitly because hacks 349 | // the slash is encoded by encodeURIComponent 350 | return a.map(pathEncoder).join(''); 351 | }; 352 | 353 | 354 | /** 355 | * Calculate the number of decimal places a number uses 356 | * We need this to get correct results out of multipleOf and divisibleBy 357 | * when either figure is has decimal places, due to IEEE-754 float issues. 358 | * @param number 359 | * @returns {number} 360 | */ 361 | exports.getDecimalPlaces = function getDecimalPlaces(number) { 362 | 363 | var decimalPlaces = 0; 364 | if (isNaN(number)) return decimalPlaces; 365 | 366 | if (typeof number !== 'number') { 367 | number = Number(number); 368 | } 369 | 370 | var parts = number.toString().split('e'); 371 | if (parts.length === 2) { 372 | if (parts[1][0] !== '-') { 373 | return decimalPlaces; 374 | } else { 375 | decimalPlaces = Number(parts[1].slice(1)); 376 | } 377 | } 378 | 379 | var decimalParts = parts[0].split('.'); 380 | if (decimalParts.length === 2) { 381 | decimalPlaces += decimalParts[1].length; 382 | } 383 | 384 | return decimalPlaces; 385 | }; 386 | 387 | exports.isSchema = function isSchema(val){ 388 | return (typeof val === 'object' && val) || (typeof val === 'boolean'); 389 | }; 390 | 391 | /** 392 | * Resolve target URL from a base and relative URL. 393 | * Similar to Node's URL Lib's legacy resolve function. 394 | * Code from example in deprecation note in said library. 395 | * @param string 396 | * @param string 397 | * @returns {string} 398 | */ 399 | var resolveUrl = exports.resolveUrl = function resolveUrl(from, to) { 400 | const resolvedUrl = new URL(to, new URL(from, 'resolve://')); 401 | if (resolvedUrl.protocol === 'resolve:') { 402 | const { pathname, search, hash } = resolvedUrl; 403 | return pathname + search + hash; 404 | } 405 | return resolvedUrl.toString(); 406 | } 407 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This is type definition for typescript. 3 | This is for library users. Thus, properties and methods for internal use is omitted. 4 | */ 5 | export declare class Validator { 6 | constructor(); 7 | customFormats: {[formatName: string]: CustomFormat}; 8 | schemas: {[id: string]: Schema}; 9 | unresolvedRefs: string[]; 10 | 11 | attributes: {[property: string]: CustomProperty}; 12 | 13 | addSchema(schema?: Schema, uri?: string): Schema|void; 14 | validate(instance: any, schema: Schema, options?: Options, ctx?: SchemaContext): ValidatorResult; 15 | } 16 | 17 | export declare class ValidatorResult { 18 | constructor(instance: any, schema: Schema, options: Options, ctx: SchemaContext) 19 | instance: any; 20 | schema: Schema; 21 | propertyPath: string; 22 | errors: ValidationError[]; 23 | throwError: boolean; 24 | disableFormat: boolean; 25 | valid: boolean; 26 | addError(detail: string|ErrorDetail): ValidationError; 27 | toString(): string; 28 | } 29 | 30 | export declare class ValidatorResultError extends Error { 31 | instance: any; 32 | schema: Schema; 33 | options: Options; 34 | errors: ValidationError; 35 | } 36 | 37 | export declare class ValidationError { 38 | constructor(message?: string, instance?: any, schema?: Schema, propertyPath?: any, name?: string, argument?: any); 39 | path: (string|number)[]; 40 | property: string; 41 | message: string; 42 | schema: string|Schema; 43 | instance: any; 44 | name: string; 45 | argument: any; 46 | toString(): string; 47 | stack: string; 48 | } 49 | 50 | export declare class SchemaError extends Error{ 51 | constructor(msg: string, schema: Schema); 52 | schema: Schema; 53 | message: string; 54 | } 55 | 56 | export declare function validate(instance: any, schema: any, options?: Options): ValidatorResult 57 | 58 | export interface Schema { 59 | $id?: string 60 | id?: string 61 | $schema?: string 62 | $ref?: string 63 | title?: string 64 | description?: string 65 | multipleOf?: number 66 | maximum?: number 67 | exclusiveMaximum?: number | boolean 68 | minimum?: number 69 | exclusiveMinimum?: number | boolean 70 | maxLength?: number 71 | minLength?: number 72 | pattern?: string | RegExp 73 | additionalItems?: boolean | Schema 74 | items?: Schema | Schema[] 75 | contains?: Schema 76 | maxItems?: number 77 | minItems?: number 78 | uniqueItems?: boolean 79 | maxProperties?: number 80 | minProperties?: number 81 | required?: string[] | boolean 82 | propertyNames?: boolean | Schema 83 | additionalProperties?: boolean | Schema 84 | definitions?: { 85 | [name: string]: Schema 86 | } 87 | properties?: { 88 | [name: string]: Schema 89 | } 90 | patternProperties?: { 91 | [name: string]: Schema 92 | } 93 | dependencies?: { 94 | [name: string]: Schema | string[] 95 | } 96 | const?: any 97 | 'enum'?: any[] 98 | type?: string | string[] 99 | format?: string 100 | allOf?: Schema[] 101 | anyOf?: Schema[] 102 | oneOf?: Schema[] 103 | not?: Schema 104 | if?: Schema 105 | then?: Schema 106 | else?: Schema 107 | default?: any 108 | examples?: any[] 109 | } 110 | 111 | export interface Options { 112 | skipAttributes?: string[]; 113 | allowUnknownAttributes?: boolean; 114 | preValidateProperty?: PreValidatePropertyFunction; 115 | rewrite?: RewriteFunction; 116 | base?: string; 117 | throwError?: boolean; 118 | required?: boolean; 119 | throwFirst?: boolean; 120 | throwAll?: boolean; 121 | nestedErrors?: boolean; 122 | } 123 | 124 | export interface RewriteFunction { 125 | (instance: any, schema: Schema, options: Options, ctx: SchemaContext): any; 126 | } 127 | 128 | export interface PreValidatePropertyFunction { 129 | (instance: any, key: string, schema: Schema, options: Options, ctx: SchemaContext): any; 130 | } 131 | 132 | export interface SchemaContext { 133 | schema: Schema; 134 | options: Options; 135 | propertyPath: string; 136 | base: string; 137 | schemas: {[base: string]: Schema}; 138 | makeChild: (schema: Schema, key: string) => SchemaContext; 139 | } 140 | 141 | export interface CustomFormat { 142 | (input: any): boolean; 143 | } 144 | 145 | export interface CustomProperty { 146 | (instance: any, schema: Schema, options: Options, ctx: SchemaContext): string|ValidatorResult; 147 | } 148 | 149 | export interface ErrorDetail { 150 | message: string; 151 | name: string; 152 | argument: string; 153 | } 154 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Validator = module.exports.Validator = require('./validator'); 4 | 5 | module.exports.ValidatorResult = require('./helpers').ValidatorResult; 6 | module.exports.ValidatorResultError = require('./helpers').ValidatorResultError; 7 | module.exports.ValidationError = require('./helpers').ValidationError; 8 | module.exports.SchemaError = require('./helpers').SchemaError; 9 | module.exports.SchemaScanResult = require('./scan').SchemaScanResult; 10 | module.exports.scan = require('./scan').scan; 11 | 12 | module.exports.validate = function (instance, schema, options) { 13 | var v = new Validator(); 14 | return v.validate(instance, schema, options); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/scan.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var helpers = require('./helpers'); 4 | 5 | module.exports.SchemaScanResult = SchemaScanResult; 6 | function SchemaScanResult(found, ref){ 7 | this.id = found; 8 | this.ref = ref; 9 | } 10 | 11 | /** 12 | * Adds a schema with a certain urn to the Validator instance. 13 | * @param string uri 14 | * @param object schema 15 | * @return {Object} 16 | */ 17 | module.exports.scan = function scan(base, schema){ 18 | function scanSchema(baseuri, schema){ 19 | if(!schema || typeof schema!='object') return; 20 | // Mark all referenced schemas so we can tell later which schemas are referred to, but never defined 21 | if(schema.$ref){ 22 | let resolvedUri = helpers.resolveUrl(baseuri,schema.$ref); 23 | ref[resolvedUri] = ref[resolvedUri] ? ref[resolvedUri]+1 : 0; 24 | return; 25 | } 26 | var id = schema.$id || schema.id; 27 | let resolvedBase = helpers.resolveUrl(baseuri,id); 28 | var ourBase = id ? resolvedBase : baseuri; 29 | if (ourBase) { 30 | // If there's no fragment, append an empty one 31 | if(ourBase.indexOf('#')<0) ourBase += '#'; 32 | if(found[ourBase]){ 33 | if(!helpers.deepCompareStrict(found[ourBase], schema)){ 34 | throw new Error('Schema <'+ourBase+'> already exists with different definition'); 35 | } 36 | return found[ourBase]; 37 | } 38 | found[ourBase] = schema; 39 | // strip trailing fragment 40 | if(ourBase[ourBase.length-1]=='#'){ 41 | found[ourBase.substring(0, ourBase.length-1)] = schema; 42 | } 43 | } 44 | scanArray(ourBase+'/items', (Array.isArray(schema.items)?schema.items:[schema.items])); 45 | scanArray(ourBase+'/extends', (Array.isArray(schema.extends)?schema.extends:[schema.extends])); 46 | scanSchema(ourBase+'/additionalItems', schema.additionalItems); 47 | scanObject(ourBase+'/properties', schema.properties); 48 | scanSchema(ourBase+'/additionalProperties', schema.additionalProperties); 49 | scanObject(ourBase+'/definitions', schema.definitions); 50 | scanObject(ourBase+'/patternProperties', schema.patternProperties); 51 | scanObject(ourBase+'/dependencies', schema.dependencies); 52 | scanArray(ourBase+'/disallow', schema.disallow); 53 | scanArray(ourBase+'/allOf', schema.allOf); 54 | scanArray(ourBase+'/anyOf', schema.anyOf); 55 | scanArray(ourBase+'/oneOf', schema.oneOf); 56 | scanSchema(ourBase+'/not', schema.not); 57 | } 58 | function scanArray(baseuri, schemas){ 59 | if(!Array.isArray(schemas)) return; 60 | for(var i=0; i", schema); 268 | } 269 | var subschema = helpers.objectGetPath(ctx.schemas[document], fragment.substr(1)); 270 | if(subschema===undefined){ 271 | throw new SchemaError("no such schema " + fragment + " located in <" + document + ">", schema); 272 | } 273 | return {subschema: subschema, switchSchema: switchSchema}; 274 | }; 275 | 276 | /** 277 | * Tests whether the instance if of a certain type. 278 | * @private 279 | * @param instance 280 | * @param schema 281 | * @param options 282 | * @param ctx 283 | * @param type 284 | * @return {boolean} 285 | */ 286 | Validator.prototype.testType = function validateType (instance, schema, options, ctx, type) { 287 | if(type===undefined){ 288 | return; 289 | }else if(type===null){ 290 | throw new SchemaError('Unexpected null in "type" keyword'); 291 | } 292 | if (typeof this.types[type] == 'function') { 293 | return this.types[type].call(this, instance); 294 | } 295 | if (type && typeof type == 'object') { 296 | var res = this.validateSchema(instance, type, options, ctx); 297 | return res === undefined || !(res && res.errors.length); 298 | } 299 | // Undefined or properties not on the list are acceptable, same as not being defined 300 | return true; 301 | }; 302 | 303 | var types = Validator.prototype.types = {}; 304 | types.string = function testString (instance) { 305 | return typeof instance == 'string'; 306 | }; 307 | types.number = function testNumber (instance) { 308 | // isFinite returns false for NaN, Infinity, and -Infinity 309 | return typeof instance == 'number' && isFinite(instance); 310 | }; 311 | types.integer = function testInteger (instance) { 312 | return (typeof instance == 'number') && instance % 1 === 0; 313 | }; 314 | types.boolean = function testBoolean (instance) { 315 | return typeof instance == 'boolean'; 316 | }; 317 | types.array = function testArray (instance) { 318 | return Array.isArray(instance); 319 | }; 320 | types['null'] = function testNull (instance) { 321 | return instance === null; 322 | }; 323 | types.date = function testDate (instance) { 324 | return instance instanceof Date; 325 | }; 326 | types.any = function testAny (instance) { 327 | return true; 328 | }; 329 | types.object = function testObject (instance) { 330 | // TODO: fix this - see #15 331 | return instance && (typeof instance === 'object') && !(Array.isArray(instance)) && !(instance instanceof Date); 332 | }; 333 | 334 | module.exports = Validator; 335 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Tom de Grunt ", 3 | "name": "jsonschema", 4 | "version": "1.5.0", 5 | "license": "MIT", 6 | "dependencies": {}, 7 | "contributors": [ 8 | { 9 | "name": "Austin Wright" 10 | } 11 | ], 12 | "main": "./lib/index.js", 13 | "typings": "./lib/index.d.ts", 14 | "devDependencies": { 15 | "@stryker-mutator/core": "^8.7.1", 16 | "@stryker-mutator/mocha-runner": "^8.7.1", 17 | "chai": "~4.2.0", 18 | "eslint": "^7.7.0", 19 | "json-metaschema": "^1.2.0", 20 | "mocha": "~11.0.1" 21 | }, 22 | "optionalDependencies": {}, 23 | "engines": { 24 | "node": "*" 25 | }, 26 | "keywords": [ 27 | "json", 28 | "schema", 29 | "jsonschema", 30 | "validator", 31 | "validation" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "git://github.com/tdegrunt/jsonschema.git" 36 | }, 37 | "description": "A fast and easy to use JSON Schema validator", 38 | "scripts": { 39 | "stryker": "stryker run", 40 | "test": "./node_modules/.bin/mocha -R spec" 41 | } 42 | } -------------------------------------------------------------------------------- /stryker.conf.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * @type {import('@stryker-mutator/api/core').StrykerOptions} 4 | */ 5 | module.exports = { 6 | packageManager: "yarn", 7 | reporters: ["html", "clear-text", "progress"], 8 | testRunner: "mocha", 9 | coverageAnalysis: "perTest", 10 | mutate: [ 11 | 'lib/*.js', 12 | ], 13 | files: [ 14 | 'lib/*.js', 15 | 'test/*.js', 16 | 'test/fixtures/*', 17 | 'test/suite/tests/**/*.json', 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /test/Validator.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | var SchemaError = require('../lib/index.js').SchemaError; 8 | var ValidationError = require('../lib/index.js').ValidationError; 9 | var ValidatorResult = require('../lib/index.js').ValidatorResult; 10 | var ValidatorResultError = require('../lib/index.js').ValidatorResultError; 11 | var assert = require('assert'); 12 | 13 | describe('Validator', function () { 14 | var validator; 15 | beforeEach(function () { 16 | validator = new Validator(); 17 | }); 18 | describe('Validator#unresolvedRefs', function(){ 19 | it('initial value', function(){ 20 | assert.deepStrictEqual(validator.unresolvedRefs, []); 21 | }); 22 | it('addSchema', function(){ 23 | validator.addSchema({ 24 | id: 'http://example.com/base.json', 25 | items: { 26 | $ref: 'item.json', 27 | }, 28 | }); 29 | assert.deepStrictEqual(validator.unresolvedRefs, ['http://example.com/item.json']); 30 | }); 31 | }); 32 | describe('Validator#addSchema', function () { 33 | it('argument schema must be a schema: object', function(){ 34 | validator.addSchema({}, 'http://example.com/base.json'); 35 | assert.deepStrictEqual(validator.schemas['http://example.com/base.json'], {}); 36 | }); 37 | // TODO: held for major version upgrade 38 | it.skip('argument schema must be a schema: true', function(){ 39 | validator.addSchema(true, 'http://example.com/base.json'); 40 | assert.deepStrictEqual(validator.schemas['http://example.com/base.json'], true); 41 | }); 42 | // TODO: held for major version upgrade 43 | it.skip('argument schema must be a schema: false', function(){ 44 | validator.addSchema(false, 'http://example.com/base.json'); 45 | assert.deepStrictEqual(validator.schemas['http://example.com/base.json'], false); 46 | }); 47 | it('argument schema must be a schema: null', function(){ 48 | var res = validator.addSchema(undefined, 'http://example.com/base.json'); 49 | assert.strictEqual(res, null); 50 | assert(!('http://example.com/base.json' in validator.schemas)); 51 | }); 52 | it('argument schema must be a schema: undefined', function(){ 53 | var res = validator.addSchema(undefined, 'http://example.com/base.json'); 54 | assert.strictEqual(res, null); 55 | assert(!('http://example.com/base.json' in validator.schemas)); 56 | }); 57 | // TODO: held for major version upgrade 58 | it.skip('argument schema must not be a number', function(){ 59 | assert.throws(function(){ 60 | validator.addSchema(1, 'http://example.com/base.json'); 61 | }, function(err){ 62 | assert(err instanceof Error); 63 | return true; 64 | }); 65 | }); 66 | // TODO: held for major version upgrade 67 | it.skip('argument schema must not be a string', function(){ 68 | assert.throws(function(){ 69 | validator.addSchema("schema", 'http://example.com/base.json'); 70 | }, function(err){ 71 | assert(err instanceof Error); 72 | return true; 73 | }); 74 | }); 75 | // TODO: held for major version upgrade 76 | it.skip('argument schema must not be null', function(){ 77 | assert.throws(function(){ 78 | validator.addSchema(null, 'http://example.com/base.json'); 79 | }, function(err){ 80 | assert(err instanceof Error); 81 | return true; 82 | }); 83 | }); 84 | // TODO: held for major version upgrade 85 | it.skip('argument schema must not be undefined', function(){ 86 | assert.throws(function(){ 87 | validator.addSchema(undefined, 'http://example.com/base.json'); 88 | }, function(err){ 89 | assert(err instanceof Error); 90 | return true; 91 | }); 92 | }); 93 | it('addSchema(schema) with absolute id', function(){ 94 | validator.addSchema({id: 'http://example.com/base.json'}); 95 | assert.deepStrictEqual(validator.unresolvedRefs, []); 96 | assert.deepStrictEqual(validator.schemas['http://example.com/base.json'], {id: 'http://example.com/base.json'}); 97 | }); 98 | it('addSchema(schema) with absolute $id', function(){ 99 | validator.addSchema({ 100 | $id: 'http://example.com/base.json', 101 | }); 102 | assert.deepStrictEqual(validator.unresolvedRefs, []); 103 | }); 104 | // TODO: held for next major version 105 | it.skip('base must be a full URI', function(){ 106 | assert.throws(function(){ 107 | validator.addSchema({id: 'main.json'}, '/index.json'); 108 | }, function(err){ 109 | assert(err instanceof SchemaError); 110 | return true; 111 | }); 112 | }); 113 | it('addSchema(schema, base) with absolute base', function(){ 114 | var res = validator.addSchema({type: 'string'}, 'http://example.com/main.json'); 115 | assert(res); 116 | assert('http://example.com/main.json' in validator.schemas); 117 | assert.deepStrictEqual(validator.schemas['http://example.com/main.json'], res); 118 | }); 119 | it('addSchema(schema, base) with absolute $id', function(){ 120 | var res = validator.addSchema({type: 'string'}, 'http://example.com/main.json'); 121 | assert(res); 122 | assert('http://example.com/main.json' in validator.schemas); 123 | assert.deepStrictEqual(validator.schemas['http://example.com/main.json'], res); 124 | }); 125 | it('addSchema(schema, base) with relative id', function(){ 126 | var res = validator.addSchema({id: 'main.json'}, 'http://example.com/index.html'); 127 | // assert(res); 128 | assert('http://example.com/main.json' in validator.schemas); 129 | }); 130 | it('addSchema(schema, base) with relative $id', function(){ 131 | var res = validator.addSchema({$id: 'main.json'}, 'http://example.com/index.html'); 132 | // assert(res); 133 | assert('http://example.com/main.json' in validator.schemas); 134 | }); 135 | it('addSchema() populates unresolvedRefs', function(){ 136 | validator.addSchema({ 137 | $id: 'main.json', 138 | items: { 139 | $ref: 'item.json', 140 | }, 141 | }, 'http://example.com/index.json'); 142 | assert('http://example.com/main.json' in validator.schemas); 143 | assert.strictEqual(validator.unresolvedRefs[0], 'http://example.com/item.json'); 144 | validator.addSchema({$id: 'item.json'}, 'http://example.com/index.json'); 145 | assert.strictEqual(validator.unresolvedRefs.length, 0); 146 | }); 147 | }); 148 | describe('Validator#validate', function () { 149 | it('schema may be an object', function () { 150 | var res = validator.validate(true, {}); 151 | assert(res.valid); 152 | }); 153 | it('schema may be true', function () { 154 | var res = validator.validate(true, true); 155 | assert(res.valid); 156 | }); 157 | it('schema may be false', function () { 158 | var res = validator.validate(true, false); 159 | assert(!res.valid); 160 | }); 161 | it('schema cannot be null', function () { 162 | assert.throws(function () { 163 | validator.validate(true, null); 164 | }, function(err){ 165 | assert(err instanceof SchemaError); 166 | assert(err.message.indexOf('object or boolean') >= 0); 167 | return true; 168 | }); 169 | }); 170 | it('schema cannot be undefined', function () { 171 | assert.throws(function () { 172 | validator.validate(true, undefined); 173 | }, function(err){ 174 | assert(err instanceof SchemaError); 175 | return true; 176 | }); 177 | }); 178 | it('schema cannot be a string', function () { 179 | assert.throws(function () { 180 | validator.validate(true, "string"); 181 | }, function(err){ 182 | assert(err instanceof SchemaError); 183 | return true; 184 | }); 185 | }); 186 | it('options may be undefined', function () { 187 | validator.validate(null, true, undefined); 188 | }); 189 | it('options may be null', function () { 190 | validator.validate(null, true, null); 191 | }); 192 | it('options.base must be a string', function () { 193 | validator.validate(null, true, null); 194 | }); 195 | it('options.required with defined instance', function () { 196 | var res = validator.validate(undefined, true, {required: true}); 197 | assert(!res.valid); 198 | assert(res.errors[0].message.indexOf('required') >= 0); 199 | var neg = validator.validate(null, true, {required: true}); 200 | assert(neg.valid); 201 | }); 202 | it('options.required with undefined instance', function () { 203 | var res = validator.validate(undefined, true, {required: true}); 204 | assert(!res.valid); 205 | assert(res.errors[0].message.indexOf('required') >= 0); 206 | var neg = validator.validate(null, true, {required: true}); 207 | assert(neg.valid); 208 | }); 209 | it('options.required is false', function () { 210 | var res = validator.validate(undefined, true, {required: false}); 211 | assert(res.valid); 212 | var neg = validator.validate(null, true, {required: true}); 213 | assert(neg.valid); 214 | }); 215 | it('options.required defaults false', function () { 216 | // TODO DEPRECATED: this behavior changes to true in next major version 217 | var res = validator.validate(undefined, true, {}); 218 | assert(res.valid); 219 | var neg = validator.validate(null, true, {required: true}); 220 | assert(neg.valid); 221 | }); 222 | it('options.throwError', function () { 223 | var schema = { 224 | properties: { 225 | "a": {type: 'number'}, 226 | "b": {type: 'number'}, 227 | }, 228 | }; 229 | var valid = {a:0, b:0}; 230 | var invalid = {a:null, b:null}; 231 | var res = validator.validate(valid, schema, {}); 232 | assert(res.valid); 233 | var neg = validator.validate(invalid, schema, {}); 234 | assert(!neg.valid); 235 | assert.throws(function(){ 236 | validator.validate(invalid, schema, {throwError: true}); 237 | }, function(err){ 238 | assert(err instanceof ValidationError); 239 | return true; 240 | }); 241 | }); 242 | it('options.throwFirst', function () { 243 | var schema = { 244 | properties: { 245 | "a": {type: 'number'}, 246 | "b": {type: 'number'}, 247 | }, 248 | }; 249 | var valid = {a:0, b:0}; 250 | var invalid = {a:null, b:null}; 251 | var res = validator.validate(valid, schema, {throwAll: true}); 252 | assert(res.valid); 253 | var neg = validator.validate(invalid, schema, {}); 254 | assert(!neg.valid); 255 | assert.throws(function(){ 256 | validator.validate(invalid, schema, {throwFirst: true}); 257 | }, function(err){ 258 | assert(err instanceof Error); 259 | assert(err instanceof ValidatorResultError); 260 | assert.strictEqual(err.errors.length, 1); 261 | return true; 262 | }); 263 | }); 264 | it('options.throwAll', function () { 265 | var schema = { 266 | properties: { 267 | "a": {type: 'number'}, 268 | "b": {type: 'number'}, 269 | }, 270 | }; 271 | var valid = {a:0, b:0}; 272 | var invalid = {a:null, b:null}; 273 | var res = validator.validate(valid, schema, {throwAll: true}); 274 | assert(res.valid); 275 | var neg = validator.validate(invalid, schema, {}); 276 | assert(!neg.valid); 277 | assert.throws(function(){ 278 | validator.validate(invalid, schema, {throwAll: true}); 279 | }, function(err){ 280 | assert(err instanceof Error); 281 | assert(err instanceof ValidatorResultError); 282 | assert.strictEqual(err.errors.length, 2); 283 | return true; 284 | }); 285 | }); 286 | it('million errors', function () { 287 | var schema = { 288 | type: 'number', 289 | oneMillionErrors: true, 290 | }; 291 | validator.attributes.oneMillionErrors = function(instance, schema, options, ctx) { 292 | const result = new ValidatorResult(instance, schema, options, ctx); 293 | for(var i = 0; i < 1000000; i++) { 294 | result.addError('oneMillionErrors error'); 295 | } 296 | return result; 297 | } 298 | var res = validator.validate(1, schema, {}); 299 | assert(!res.valid); 300 | assert.strictEqual(res.errors.length, 1000000); 301 | }); 302 | it('subschema references (named reference)', function () { 303 | var schema = { 304 | items: {$ref: '#items'}, 305 | definitions: { 306 | items: { 307 | $id: '#items', 308 | type: 'array', 309 | }, 310 | }, 311 | }; 312 | var res = validator.validate([[]], schema); 313 | assert(res.valid); 314 | var neg = validator.validate([null], schema); 315 | assert(!neg.valid); 316 | }); 317 | it('subschema references (path reference)', function () { 318 | var schema = { 319 | items: {$ref: '#/definitions/items'}, 320 | definitions: { 321 | items: { 322 | type: 'array', 323 | }, 324 | }, 325 | }; 326 | var res = validator.validate([[]], schema); 327 | assert(res.valid); 328 | var neg = validator.validate([null], schema); 329 | assert(!neg.valid); 330 | }); 331 | it('recursive references (fragment reference)', function () { 332 | var schema = { 333 | $id: 'http://example.com/foo.json', 334 | items: {$ref: '#'}, 335 | type: 'array', 336 | }; 337 | var res = validator.validate([[[[]]]], schema); 338 | assert(res.valid); 339 | var neg = validator.validate([null], schema); 340 | assert(!neg.valid); 341 | }); 342 | it('recursive references (filename reference)', function () { 343 | var schema = { 344 | $id: 'http://example.com/foo.json', 345 | items: {$ref: 'foo.json'}, 346 | type: 'array', 347 | }; 348 | var res = validator.validate([[[[]]]], schema); 349 | assert(res.valid); 350 | var neg = validator.validate([null], schema); 351 | assert(!neg.valid); 352 | }); 353 | }); 354 | }); 355 | -------------------------------------------------------------------------------- /test/arrays.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/validator'); 7 | 8 | require('chai').should(); 9 | 10 | describe('Arrays', function () { 11 | describe('simple array', function () { 12 | beforeEach(function () { 13 | this.validator = new Validator(); 14 | }); 15 | 16 | it('should validate an empty array', function () { 17 | this.validator.validate([], {'type': 'array', 'items': {'type': 'string'}}); 18 | }); 19 | 20 | it('should validate an undefined array', function () { 21 | this.validator.validate(undefined, {'type': 'array', 'items': {'type': 'string'}}).valid.should.be.true; 22 | }); 23 | 24 | it('should validate an array with strings', function () { 25 | this.validator.validate(['1', '2', '3'], {'type': 'array', 'items': {'type': 'string'}}).valid.should.be.true; 26 | }); 27 | 28 | it('should not validate an array not all strings', function () { 29 | var res = this.validator.validate(['1', '2', '3', 4], {'type': 'array', 'items': {'type': 'string'}}); 30 | res.valid.should.be.false; 31 | res.errors.length.should.equal(1); 32 | res.errors[0].property.should.equal('instance[3]'); 33 | res.errors[0].path.length.should.equal(1); 34 | res.errors[0].path[0].should.equal(3); 35 | }); 36 | 37 | it('should not validate a non-array', function () { 38 | return this.validator.validate(0, {'type': 'array'}).valid.should.be.false; 39 | }); 40 | }); 41 | 42 | // Perhaps these should be tested in attributes, but they fit more with arrays 43 | describe('minItems', function () { 44 | beforeEach(function () { 45 | this.validator = new Validator(); 46 | }); 47 | 48 | it('should validate if array has a length equal to minItems', function () { 49 | return this.validator.validate([1, 2, 3], {'type': 'array', 'items': {'type': 'number'}, 'minItems': 2}).valid.should.be.true; 50 | }); 51 | 52 | it('should validate if array has a length equal to minItems', function () { 53 | return this.validator.validate([1, 2], {'type': 'array', 'items': {'type': 'number'}, 'minItems': 2}).valid.should.be.true; 54 | }); 55 | 56 | it('should not validate if array has a length less than minItems', function () { 57 | var res = this.validator.validate([1], {'type': 'array', 'items': {'type': 'number'}, 'minItems': 2}); 58 | res.valid.should.be.false; 59 | res.errors[0].name.should.equal('minItems'); 60 | res.errors[0].property.should.equal('instance'); 61 | res.errors[0].path.length.should.equal(0); 62 | }); 63 | }); 64 | 65 | describe('maxItems', function () { 66 | beforeEach(function () { 67 | this.validator = new Validator(); 68 | }); 69 | 70 | it('should validate if array has a length less than maxItems', function () { 71 | return this.validator.validate([1], {'type': 'array', 'items': {'type': 'number'}, 'maxItems': 2}).valid.should.be.true; 72 | }); 73 | 74 | it('should validate if array has a length equal to maxItems', function () { 75 | return this.validator.validate([1, 2], {'type': 'array', 'items': {'type': 'number'}, 'maxItems': 2}).valid.should.be.true; 76 | }); 77 | 78 | it('should validate if array has a length larger than maxItems', function () { 79 | return this.validator.validate([1, 2, 3], {'type': 'array', 'items': {'type': 'number'}, 'maxItems': 2}).valid.should.be.false; 80 | }); 81 | }); 82 | 83 | describe('uniqueItems', function () { 84 | beforeEach(function () { 85 | this.validator = new Validator(); 86 | }); 87 | 88 | it('should validate if array has no duplicate items', function () { 89 | return this.validator.validate([1], {'type': 'array', 'uniqueItems': true}).valid.should.be.true; 90 | }); 91 | 92 | it('should validate if array has no duplicate objects', function () { 93 | return this.validator.validate([1, 2, "1", "2", {a:1}, {a:1, b:1}], {'type': 'array', 'uniqueItems': true}).valid.should.be.true; 94 | }); 95 | 96 | it('should not validate if array has duplicate numbers', function () { 97 | return this.validator.validate([1, 2, 4, 1, 3, 5], {'type': 'array', 'uniqueItems': true}).valid.should.be.false; 98 | }); 99 | 100 | it('should not validate if array has duplicate objects', function () { 101 | return this.validator.validate([{a:1}, {a:1}], {'type': 'array', 'uniqueItems': true}).valid.should.be.false; 102 | }); 103 | 104 | it('should validate if not an Array', function () { 105 | return this.validator.validate(null, {'type': 'any', 'uniqueItems': true}).valid.should.be.true; 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/attributes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | var SchemaError = require('../lib/index.js').SchemaError; 8 | var assert = require('assert'); 9 | 10 | require('chai').should(); 11 | 12 | describe('Attributes', function () { 13 | describe('type', function () { 14 | beforeEach(function () { 15 | this.validator = new Validator(); 16 | }); 17 | 18 | describe('number', function () { 19 | it('should validate a valid number', function () { 20 | this.validator.validate(0, {'type': 'number'}).valid.should.be.true; 21 | }); 22 | 23 | it('should not validate an invalid number', function () { 24 | return this.validator.validate('0', {'type': 'number'}).valid.should.be.false; 25 | }); 26 | 27 | it('should not validate NaN', function () { 28 | return this.validator.validate(NaN, {'type': 'number'}).valid.should.be.false; 29 | }); 30 | 31 | it('should not validate Infinity', function () { 32 | return this.validator.validate(Infinity, {'type': 'number'}).valid.should.be.false; 33 | }); 34 | 35 | it('should not validate -Infinity', function () { 36 | return this.validator.validate(-Infinity, {'type': 'number'}).valid.should.be.false; 37 | }); 38 | 39 | }); 40 | 41 | describe('required', function () { 42 | it('should not validate an undefined instance', function () { 43 | this.validator.validate(undefined, {'type': 'number', 'required': true}).valid.should.be.false; 44 | }); 45 | }); 46 | 47 | describe('null', function () { 48 | 49 | it('should validate null', function () { 50 | return this.validator.validate(null, {'type': 'null'}).valid.should.be.true; 51 | }); 52 | 53 | it('should not validate no-null', function () { 54 | return this.validator.validate('0', {'type': 'null'}).valid.should.be.false; 55 | }); 56 | 57 | // I don't know - strictly undefined should not be a valid null 58 | it('should not validate an undefined instance', function () { 59 | this.validator.validate(undefined, {'type': 'date', 'required': true}).valid.should.be.false; 60 | }); 61 | }); 62 | 63 | describe('date', function () { 64 | 65 | it('should validate date', function () { 66 | return this.validator.validate(new Date(), {'type': 'date'}).valid.should.be.true; 67 | }); 68 | 69 | it('should not validate no-null', function () { 70 | return this.validator.validate('0', {'type': 'date'}).valid.should.be.false; 71 | }); 72 | 73 | it('should not validate an undefined instance', function () { 74 | this.validator.validate(undefined, {'type': 'date', 'required': true}).valid.should.be.false; 75 | }); 76 | }); 77 | 78 | describe('integer', function () { 79 | 80 | it('should validate integer', function () { 81 | return this.validator.validate(12, {'type': 'integer'}).valid.should.be.true; 82 | }); 83 | 84 | it('should not validate non integer', function () { 85 | return this.validator.validate(0.25, {'type': 'integer'}).valid.should.be.false; 86 | }); 87 | 88 | it('should not validate an undefined instance', function () { 89 | this.validator.validate(undefined, {'type': 'integer', 'required': true}).valid.should.be.false; 90 | }); 91 | }); 92 | 93 | describe('boolean', function () { 94 | 95 | it('should validate true', function () { 96 | return this.validator.validate(true, {'type': 'boolean'}).valid.should.be.true; 97 | }); 98 | 99 | it('should validate false', function () { 100 | return this.validator.validate(false, {'type': 'boolean'}).valid.should.be.true; 101 | }); 102 | 103 | it('should not validate non boolean', function () { 104 | return this.validator.validate('true', {'type': 'boolean'}).valid.should.be.false; 105 | }); 106 | 107 | it('should not validate an undefined instance', function () { 108 | this.validator.validate(undefined, {'type': 'boolean', 'required': true}).valid.should.be.false; 109 | }); 110 | }); 111 | 112 | describe('any', function () { 113 | 114 | it('should validate true as any', function () { 115 | return this.validator.validate(true, {'type': 'any'}).valid.should.be.true; 116 | }); 117 | 118 | it('should validate "true" as any', function () { 119 | return this.validator.validate('true', {'type': 'any'}).valid.should.be.true; 120 | }); 121 | 122 | it('should validate 0 as any', function () { 123 | return this.validator.validate(0, {'type': 'any'}).valid.should.be.true; 124 | }); 125 | 126 | it('should validate Date as any', function () { 127 | return this.validator.validate(new Date(), {'type': 'any'}).valid.should.be.true; 128 | }); 129 | 130 | it('should not validate an undefined instance', function () { 131 | this.validator.validate(undefined, {'type': 'any', 'required': true}).valid.should.be.false; 132 | }); 133 | }); 134 | describe('invalid values', function () { 135 | it('should ignore undefined', function () { 136 | var schema = { 137 | type: [undefined, "string"], 138 | }; 139 | this.validator.validate(12, schema).valid.should.be.false; 140 | this.validator.validate("foo", schema).valid.should.be.true; 141 | }); 142 | 143 | it('should error on null', function () { 144 | var validator = this.validator; 145 | var schema = { 146 | type: [null, "string"], 147 | }; 148 | assert.throws(function(){ 149 | validator.validate("foo", schema); 150 | }, function(err){ 151 | assert(err instanceof SchemaError); 152 | assert.strictEqual(err.message, 'Unexpected null in "type" keyword'); 153 | return true; 154 | }); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('oneOf', function () { 160 | var schema = { 161 | oneOf: [ 162 | {type: 'string'}, 163 | {enum: [0, 1]}, 164 | {type: 'object', required: ['type']}, 165 | {type: 'object', required: ['name']}, 166 | ], 167 | }; 168 | beforeEach(function () { 169 | this.validator = new Validator(); 170 | }); 171 | 172 | it('does not validate zero successes', function () { 173 | return this.validator.validate(true, schema).valid.should.be.false; 174 | }); 175 | 176 | it('validates one success', function () { 177 | return this.validator.validate({type: true}, schema).valid.should.be.true; 178 | }); 179 | 180 | it('does not validate two successes', function () { 181 | return this.validator.validate({type:true, name:true}, schema).valid.should.be.false; 182 | }); 183 | 184 | it('reports inner errors with nestedErrors flag', function () { 185 | return this.validator.validate({type:true, name:true}, schema, {nestedErrors:true}).errors.length.should.equal(3); 186 | }); 187 | 188 | it('functions with throwError flag', function () { 189 | var validator = this.validator; 190 | assert.throws(function(){ 191 | validator.validate({type:true, name:true}, schema, {throwError:true}); 192 | }, function(err){ 193 | err.message.should.contain('exactly one'); 194 | return true; 195 | }); 196 | assert.doesNotThrow(function(){ 197 | validator.validate({type:true}, schema, {throwError:true}); 198 | }); 199 | }); 200 | 201 | }); 202 | 203 | describe('minimum', function () { 204 | beforeEach(function () { 205 | this.validator = new Validator(); 206 | }); 207 | 208 | it('should validate if number meets minimum', function () { 209 | return this.validator.validate(1, {'type': 'number', 'minimum': '1'}).valid.should.be.true; 210 | }); 211 | 212 | it('should not validate if number is below minimum', function () { 213 | return this.validator.validate(0, {'type': 'number', 'minimum': '1'}).valid.should.be.false; 214 | }); 215 | 216 | it('should validate if number is above minimum, using exclusiveMinimum', function () { 217 | return this.validator.validate(1, {'type': 'number', 'minimum': '0', 'exclusiveMinimum': true}).valid.should.be.true; 218 | }); 219 | 220 | it('should not validate if number is the minimum, using exclusiveMinimum', function () { 221 | return this.validator.validate(1, {'type': 'number', 'minimum': '1', 'exclusiveMinimum': true}).valid.should.be.false; 222 | }); 223 | 224 | }); 225 | 226 | describe('maximum', function () { 227 | beforeEach(function () { 228 | this.validator = new Validator(); 229 | }); 230 | 231 | it('should validate if number is below the maximum', function () { 232 | return this.validator.validate(1, {'type': 'number', 'maximum': '2'}).valid.should.be.true; 233 | }); 234 | 235 | it('should not validate if number is above maximum', function () { 236 | return this.validator.validate(3, {'type': 'number', 'maximum': '2'}).valid.should.be.false; 237 | }); 238 | 239 | it('should validate if number is below maximum, using exclusiveMinimum', function () { 240 | return this.validator.validate(1, {'type': 'number', 'maximum': '2', 'exclusiveMaximum': true}).valid.should.be.true; 241 | }); 242 | 243 | it('should not validate if number is the maximum, using exclusiveMinimum', function () { 244 | return this.validator.validate(2, {'type': 'number', 'maximum': '2', 'exclusiveMaximum': true}).valid.should.be.false; 245 | }); 246 | 247 | }); 248 | 249 | describe('combined minimum and maximum', function () { 250 | beforeEach(function () { 251 | this.validator = new Validator(); 252 | }); 253 | 254 | it('should validate if number is below the maximum', function () { 255 | return this.validator.validate(1, {'type': 'number', 'minimum': '1', 'maximum': '2'}).valid.should.be.true; 256 | }); 257 | 258 | it('should not validate if number is above minumum', function () { 259 | this.validator.validate(3, {'type': 'number', 'minimum': '1', 'maximum': '2'}).valid.should.be.false; 260 | }); 261 | }); 262 | 263 | describe('divisibleBy', function () { 264 | beforeEach(function () { 265 | this.validator = new Validator(); 266 | }); 267 | 268 | it('should validate if 0 is even', function () { 269 | return this.validator.validate(2, {'type': 'number', 'divisibleBy': 2}).valid.should.be.true; 270 | }); 271 | 272 | it('should validate if -2 is even', function () { 273 | return this.validator.validate(-2, {'type': 'number', 'divisibleBy': 2}).valid.should.be.true; 274 | }); 275 | 276 | it('should not validate 1 is even', function () { 277 | return this.validator.validate(1, {'type': 'number', 'divisibleBy': 2}).valid.should.be.false; 278 | }); 279 | 280 | it('should validate divisibleBy with decimals', function () { 281 | return this.validator.validate(2.4, {'type': 'number', 'divisibleBy': 0.1}).valid.should.be.true; 282 | }); 283 | }); 284 | 285 | describe('multipleOf', function () { 286 | beforeEach(function () { 287 | this.validator = new Validator(); 288 | }); 289 | 290 | it('should validate if 0 is even', function () { 291 | return this.validator.validate(2, {'type': 'number', 'multipleOf': 2}).valid.should.be.true; 292 | }); 293 | 294 | it('should validate if -2 is even', function () { 295 | return this.validator.validate(-2, {'type': 'number', 'multipleOf': 2}).valid.should.be.true; 296 | }); 297 | 298 | it('should not validate 1 is even', function () { 299 | return this.validator.validate(1, {'type': 'number', 'multipleOf': 2}).valid.should.be.false; 300 | }); 301 | 302 | it('should validate mutlipleOf with decimals', function () { 303 | return this.validator.validate(2.4, {'type': 'number', 'multipleOf': 0.1}).valid.should.be.true; 304 | }); 305 | }); 306 | 307 | describe('pattern', function () { 308 | beforeEach(function () { 309 | this.validator = new Validator(); 310 | }); 311 | 312 | it('should validate if string matches the string pattern', function () { 313 | return this.validator.validate('abbbc', {'type': 'string', 'pattern': 'ab+c'}).valid.should.be.true; 314 | }); 315 | 316 | it('should validate if string matches the regexp pattern', function () { 317 | return this.validator.validate('abbbc', {'type': 'string', 'pattern': /ab+c/}).valid.should.be.true; 318 | }); 319 | 320 | it('should validate if string does not match the string pattern', function () { 321 | return this.validator.validate('abac', {'type': 'string', 'pattern': 'ab+c'}).valid.should.be.false; 322 | }); 323 | 324 | it('should return correct error message when parsing regular expression', function () { 325 | return this.validator.validate('abac', {'type': 'string', 'pattern': /^a+$/}).errors[0].stack.should.include("/^a+$/"); 326 | }); 327 | 328 | it('supports invalid non-unicode patterns (deprecated)', function () { 329 | this.validator.validate('0{012}', {'type': 'string', 'pattern': /0{1.3}/}).valid.should.be.false; 330 | this.validator.validate('0{123}', {'type': 'string', 'pattern': /0{1.3}/}).valid.should.be.true; 331 | this.validator.validate('0{012}', {'type': 'string', 'pattern': "0{1.3}"}).valid.should.be.false; 332 | this.validator.validate('0{123}', {'type': 'string', 'pattern': "0{1.3}"}).valid.should.be.true; 333 | }); 334 | }); 335 | 336 | describe('minLength', function () { 337 | beforeEach(function () { 338 | this.validator = new Validator(); 339 | }); 340 | 341 | it('should validate if string has a length larger than minLength', function () { 342 | return this.validator.validate('abcde', {'type': 'string', 'minLength': 5}).valid.should.be.true; 343 | }); 344 | 345 | it('should not validate if string does has a length less than minLength', function () { 346 | return this.validator.validate('abcde', {'type': 'string', 'minLength': 6}).valid.should.be.false; 347 | }); 348 | }); 349 | 350 | describe('maxLength', function () { 351 | beforeEach(function () { 352 | this.validator = new Validator(); 353 | }); 354 | 355 | it('should validate if string has a length equal to maxLength', function () { 356 | return this.validator.validate('abcde', {'type': 'string', 'maxLength': 5}).valid.should.be.true; 357 | }); 358 | 359 | it('should not validate if string does has a length larger than maxLength', function () { 360 | return this.validator.validate('abcde', {'type': 'string', 'maxLength': 4}).valid.should.be.false; 361 | }); 362 | }); 363 | 364 | describe('enum', function () { 365 | beforeEach(function () { 366 | this.validator = new Validator(); 367 | }); 368 | 369 | it('should validate if string is one of the enum values', function () { 370 | return this.validator.validate('abcde', {'type': 'string', 'enum': ['abcdf', 'abcde']}).valid.should.be.true; 371 | }); 372 | 373 | it('should not validate if string is not one of the enum values', function () { 374 | return this.validator.validate('abcde', {'type': 'string', 'enum': ['abcdf', 'abcdd']}).valid.should.be.false; 375 | }); 376 | 377 | it('should validate if number is one of the enum values', function () { 378 | return this.validator.validate(1, {'type': 'number', 'enum': [1, 2]}).valid.should.be.true; 379 | }); 380 | 381 | it('should not validate if number is not one of the enum values', function () { 382 | return this.validator.validate(3, {'type': 'string', 'enum': [1, 2]}).valid.should.be.false; 383 | }); 384 | 385 | it('should validate if value is undefined but defaults to one of the enum values', function () { 386 | return this.validator.validate(undefined, {'enum': ['foo', 'bar', 'baz'], 'default': 'baz'}).valid.should.be.true; 387 | }); 388 | 389 | it('should not validate if value is undefined and required, even if a default is given', function () { 390 | return this.validator.validate(undefined, {'enum': ['foo', 'bar', 'baz'], 'required': true, 'default': 'baz'}).valid.should.be.false; 391 | }); 392 | 393 | it('should not validate if a required field is ommited', function () { 394 | return this.validator.validate({}, {'type': 'object', 'properties':{'the_field': {'enum': ['foo', 'bar', 'baz'], 'required': true}}}).valid.should.be.false; 395 | }); 396 | 397 | it('should not validate if a required field is undefined', function () { 398 | return this.validator.validate({'the_field':undefined}, {'type': 'object', 'properties':{'the_field': {'enum': ['foo', 'bar', 'baz'], 'required': true}}}).valid.should.be.false; 399 | }); 400 | 401 | it('should not validate if a field in required array is undefined', function () { 402 | return this.validator.validate({'the_field':undefined}, {'type': 'object', 'properties':{'the_field': {'enum': ['foo', 'bar', 'baz'] }}, required: ['the_field']}).valid.should.be.false; 403 | }); 404 | 405 | it('should validate if a required field has a value out of enum', function () { 406 | return this.validator.validate({'the_field':'bar'}, {'type': 'object', 'properties':{'the_field': {'enum': ['foo', 'bar', 'baz'], 'required': true}}}).valid.should.be.true; 407 | }); 408 | }); 409 | 410 | describe('description', function () { 411 | beforeEach(function () { 412 | this.validator = new Validator(); 413 | }); 414 | 415 | it('should be ignored', function () { 416 | this.validator.validate(1, {'description': 'some text'}).valid.should.be.true; 417 | }); 418 | }); 419 | 420 | describe('disallow', function () { 421 | beforeEach(function () { 422 | this.validator = new Validator(); 423 | }); 424 | 425 | it('should prohibit specified types', function () { 426 | this.validator.validate(1, {'type': 'any', 'disallow':'array'}).valid.should.be.true; 427 | }); 428 | 429 | it('should not prohibit unprohibited types', function () { 430 | this.validator.validate(1, {'type':'any', 'disallow':'array'}).valid.should.be.true; 431 | }); 432 | }); 433 | 434 | describe('dependencies', function () { 435 | beforeEach(function () { 436 | this.validator = new Validator(); 437 | }); 438 | 439 | it('should validate with missing non-depended properties', function () { 440 | this.validator.validate({foo: 1}, {'dependencies': {'quux': ['foo', 'bar']}}).valid.should.be.true; 441 | }); 442 | 443 | it('should not validate with missing dependencies', function () { 444 | this.validator.validate({quux: 1, foo: 1}, {'dependencies': {'quux': ['foo', 'bar']}}).valid.should.be.false; 445 | }); 446 | 447 | it('should validate with satisfied dependencies', function () { 448 | this.validator.validate({quux: 1, foo: 1, bar: 1}, {'dependencies': {'quux': ['foo', 'bar']}}).valid.should.be.true; 449 | }); 450 | }); 451 | }); 452 | -------------------------------------------------------------------------------- /test/combinators.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | 8 | require('chai').should(); 9 | 10 | describe('Combinators', function () { 11 | beforeEach(function () { 12 | this.validator = new Validator(); 13 | }); 14 | 15 | describe('anyOf', function () { 16 | beforeEach(function () { 17 | this.schema = { 18 | 'type': 'object', 19 | 'anyOf': [{ 20 | 'properties': { 21 | 'name': {'type': 'string', 'enum': ['test1'] }, 22 | }, 23 | }, { 24 | 'properties': { 25 | 'name': {'type': 'string', 'enum': ['test2'] }, 26 | }, 27 | }], 28 | }; 29 | }); 30 | 31 | it('should validate if matches any of', function () { 32 | this.validator.validate({ 'name': 'test2' }, this.schema).valid.should.be.true; 33 | }); 34 | 35 | it('should not validate if not match any of', function () { 36 | this.validator.validate({ 'name': 'test3' }, this.schema).valid.should.be.false; 37 | }); 38 | 39 | it('should not throw if valid when throwError is set', function () { 40 | (function() { 41 | this.validator.validate({ 'name': 'test2' }, this.schema, { throwError: true }); 42 | }.bind(this)).should.not.throw(); 43 | }); 44 | 45 | it('should throw if invalid when throwError is set', function () { 46 | (function() { 47 | this.validator.validate({ 'name': 'test3' }, this.schema, { throwError: true }); 48 | }.bind(this)).should.throw(); 49 | }); 50 | }); 51 | 52 | describe('oneOf', function () { 53 | beforeEach(function () { 54 | this.schema = { 55 | 'type': 'object', 56 | 'oneOf': [{ 57 | 'properties': { 58 | 'name1': {'type': 'string', 'enum': ['test1'] }, 59 | }, 60 | 'additionalProperties': false, 61 | }, { 62 | 'properties': { 63 | 'name2': {'type': 'string', 'enum': ['test2'] }, 64 | }, 65 | 'additionalProperties': false, 66 | }], 67 | }; 68 | }); 69 | 70 | it('should validate if matches exactly one of', function () { 71 | this.validator.validate({ 'name2': 'test2' }, this.schema).valid.should.be.true; 72 | }); 73 | 74 | it('should not validate if not match exactly one of', function () { 75 | this.validator.validate({ 'name1': 'test1', 'name2': 'test2' }, this.schema).valid.should.be.false; 76 | }); 77 | 78 | it('should not throw if valid when throwError is set', function () { 79 | (function() { 80 | this.validator.validate({ 'name2': 'test2' }, this.schema, { throwError: true }); 81 | }.bind(this)).should.not.throw(); 82 | }); 83 | 84 | it('should throw if invalid when throwError is set', function () { 85 | (function() { 86 | this.validator.validate({ 'name1': 'test1', 'name2': 'test2' }, this.schema, { throwError: true }); 87 | }.bind(this)).should.throw(); 88 | }); 89 | }); 90 | 91 | }); 92 | -------------------------------------------------------------------------------- /test/const.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | // Adds preliminary tests for the "const" keyword expected to be added to JSON Schema with this behavior 7 | 8 | var Validator = require('../lib/index.js').Validator; 9 | var should = require('chai').should(); 10 | 11 | describe('"const" keyword', function () { 12 | beforeEach(function () { 13 | this.validator = new Validator(); 14 | }); 15 | 16 | describe('string', function () { 17 | var schema = { 'const': 'value' }; 18 | it('valid', function () { 19 | this.validator.validate("value", schema).valid.should.be.true; 20 | }); 21 | it('invalid 1', function () { 22 | this.validator.validate("invalid 1", schema).valid.should.be.false; 23 | }); 24 | it('invalid 2', function () { 25 | this.validator.validate("", schema).valid.should.be.false; 26 | }); 27 | }); 28 | 29 | describe('object', function () { 30 | var schema = { 'const': {"some key": [ null, "1", 2, true ]} }; 31 | it('valid', function () { 32 | this.validator.validate({"some key": [ null, "1", 2, true ]}, schema).valid.should.be.true; 33 | }); 34 | it('invalid 1', function () { 35 | this.validator.validate([null], schema).valid.should.be.false; 36 | }); 37 | it('invalid 2', function () { 38 | this.validator.validate({"some key": [ false, "1", 2, true ]}, schema).valid.should.be.false; 39 | }); 40 | it('invalid 3', function () { 41 | this.validator.validate(true, schema).valid.should.be.false; 42 | }); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /test/fixtures/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "payment": { 3 | "amount":5000, 4 | "other_amount":100, 5 | "usage":"Order 12345 pucrchase of concert tickets" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/data_collection.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "payment": { 4 | "amount":5000, 5 | "other_amount":100, 6 | "usage":"Order 12345 purchase of concert tickets" 7 | } 8 | }, 9 | { 10 | "payment": { 11 | "amount":10, 12 | "other_amount":20, 13 | "usage":"Order 12346 purchase of one teddy bear" 14 | } 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /test/fixtures/data_collection_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "data_collection_schema.json", 3 | "title": "Collection of data", 4 | "type": "array", 5 | "items": { 6 | "$ref": "/data_schema.json" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/data_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "description" : "Application Data", 3 | "type":"object", 4 | "$schema": "http://json-schema.org/draft-03/schema", 5 | "required":true, 6 | "properties": { 7 | "payment": { 8 | "type":"object", 9 | "required":true, 10 | "additionalProperties":false, 11 | "properties": { 12 | "amount": { 13 | "required":true, 14 | "extends" : "types.json#/amount" 15 | }, 16 | "other_amount": { 17 | "required":true, 18 | "extends" : {"$ref": "types.json#/amount"} 19 | }, 20 | "usage": { 21 | "$ref": "types.json#/usage" 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /test/fixtures/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "description" : "custom types", 3 | "type":"object", 4 | "amount": { 5 | "description" : "Amount of the payment in cents, must be > 0", 6 | "type" : "integer", 7 | "minimum": 1, 8 | "exclusiveMinimum": false 9 | }, 10 | "usage": { 11 | "description" : "text statement for bank statement", 12 | "type" : "string", 13 | "minLength": 1, 14 | "maxLength": 255 15 | } 16 | } -------------------------------------------------------------------------------- /test/formats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | 8 | require('chai').should(); 9 | 10 | describe('Formats', function () { 11 | beforeEach(function () { 12 | this.validator = new Validator(); 13 | }); 14 | 15 | describe('date-time', function () { 16 | it('should validate a valid date-time', function () { 17 | this.validator.validate("2012-07-08T16:41:41.532Z", {'type': 'string', 'format': 'date-time'}).valid.should.be.true; 18 | }); 19 | 20 | it('should validate a valid date-time without milliseconds', function () { 21 | this.validator.validate("2012-07-08T16:41:41Z", {'type': 'string', 'format': 'date-time'}).valid.should.be.true; 22 | }); 23 | 24 | it('should validate a date-time with a timezone offset instead of Z', function () { 25 | this.validator.validate("2012-07-08T16:41:41.532+00:00", {'type': 'string', 'format': 'date-time'}).valid.should.be.true; 26 | this.validator.validate("2012-07-08T16:41:41.532+05:30", {'type': 'string', 'format': 'date-time'}).valid.should.be.true; 27 | this.validator.validate("2012-07-08T16:41:41.532+04:00", {'type': 'string', 'format': 'date-time'}).valid.should.be.true; 28 | }); 29 | 30 | it('should validate a date-time with a z instead of a Z', function () { 31 | this.validator.validate("2012-07-08T16:41:41.532z", {'type': 'string', 'format': 'date-time'}).valid.should.be.true; 32 | }); 33 | 34 | it('should validate a date-time with a space instead of a T', function () { 35 | this.validator.validate("2012-07-08 16:41:41.532Z", {'type': 'string', 'format': 'date-time'}).valid.should.be.true; 36 | }); 37 | 38 | it('should validate a date-time with a t instead of a T', function () { 39 | this.validator.validate("2012-07-08t16:41:41.532Z", {'type': 'string', 'format': 'date-time'}).valid.should.be.true; 40 | }); 41 | 42 | it('should not validate a date-time with the time missing', function () { 43 | this.validator.validate("2012-07-08", {'type': 'string', 'format': 'date-time'}).valid.should.be.false; 44 | }); 45 | 46 | it('should not validate an invalid date-time', function () { 47 | this.validator.validate("TEST2012-07-08T16:41:41.532Z", {'type': 'string', 'format': 'date-time'}).valid.should.be.false; 48 | }); 49 | 50 | it('should not validate a date-time with a timezone offset AND a Z', function () { 51 | this.validator.validate("2012-07-08T16:41:41.532+00:00Z", {'type': 'string', 'format': 'date-time'}).valid.should.be.false; 52 | this.validator.validate("2012-07-08T16:41:41.532+Z00:00", {'type': 'string', 'format': 'date-time'}).valid.should.be.false; 53 | }); 54 | }); 55 | 56 | describe('date', function () { 57 | it('should validate a valid date', function () { 58 | this.validator.validate("2012-07-08", {'type': 'string', 'format': 'date'}).valid.should.be.true; 59 | }); 60 | 61 | it('should not validate an invalid date', function () { 62 | this.validator.validate("TEST2012-07-08", {'type': 'string', 'format': 'date'}).valid.should.be.false; 63 | }); 64 | 65 | }); 66 | 67 | describe('time', function () { 68 | it('should validate a valid time', function () { 69 | this.validator.validate("16:41:41", {'type': 'string', 'format': 'time'}).valid.should.be.true; 70 | }); 71 | 72 | it('should not validate an invalid time', function () { 73 | this.validator.validate("16:41:41.532Z", {'type': 'string', 'format': 'time'}).valid.should.be.false; 74 | }); 75 | 76 | }); 77 | 78 | describe('utc-millisec', function () { 79 | it('should validate a valid utc-millisec', function () { 80 | this.validator.validate("-1234567890", {'type': 'string', 'format': 'utc-millisec'}).valid.should.be.true; 81 | }); 82 | 83 | it('should not validate an invalid utc-millisec', function () { 84 | this.validator.validate("16:41:41.532Z", {'type': 'string', 'format': 'utc-millisec'}).valid.should.be.false; 85 | }); 86 | 87 | }); 88 | 89 | describe('regex', function () { 90 | it('should validate a valid regex', function () { 91 | this.validator.validate("/a/", {'type': 'string', 'format': 'regex'}).valid.should.be.true; 92 | }); 93 | 94 | it('should not validate an invalid regex', function () { 95 | this.validator.validate("/^(abc]/", {'type': 'string', 'format': 'regex'}).valid.should.be.false; 96 | }); 97 | }); 98 | 99 | describe('color', function () { 100 | it('should validate the color red', function () { 101 | this.validator.validate("red", {'type': 'string', 'format': 'color'}).valid.should.be.true; 102 | }); 103 | 104 | it('should validate the color #f00', function () { 105 | this.validator.validate("#f00", {'type': 'string', 'format': 'color'}).valid.should.be.true; 106 | }); 107 | 108 | it('should validate the color #ff0000', function () { 109 | this.validator.validate("#ff0000", {'type': 'string', 'format': 'color'}).valid.should.be.true; 110 | }); 111 | 112 | it('should validate the color rgb(255,0,0)', function () { 113 | this.validator.validate("rgb(255,0,0)", {'type': 'string', 'format': 'color'}).valid.should.be.true; 114 | }); 115 | 116 | it('should not validate an invalid color (json)', function () { 117 | this.validator.validate("json", {'type': 'string', 'format': 'color'}).valid.should.be.false; 118 | }); 119 | }); 120 | 121 | describe('style', function () { 122 | it('should validate a valid style', function () { 123 | this.validator.validate("color: red;", {'type': 'string', 'format': 'style'}).valid.should.be.true; 124 | }); 125 | 126 | it('should validate a valid complex style', function () { 127 | this.validator.validate("color: red; position: absolute; background-color: rgb(204, 204, 204); max-width: 150px;", {'type': 'string', 'format': 'style'}).valid.should.be.true; 128 | }); 129 | 130 | it('should validate a valid complex style', function () { 131 | this.validator.validate("color:red;position:absolute; background-color: rgb(204, 204, 204); max-width: 150px;", {'type': 'string', 'format': 'style'}).valid.should.be.true; 132 | }); 133 | 134 | it('should not validate an invalid style', function () { 135 | this.validator.validate("0", {'type': 'string', 'format': 'style'}).valid.should.be.false; 136 | }); 137 | 138 | it('should validate a valid style if called twice with the same instance', function () { 139 | this.validator.validate("color: red;", {'type': 'string', 'format': 'style'}).valid.should.be.true; 140 | this.validator.validate("color: red;", {'type': 'string', 'format': 'style'}).valid.should.be.true; 141 | }); 142 | 143 | }); 144 | 145 | describe('phone', function () { 146 | it('should validate a valid phone-number', function () { 147 | this.validator.validate("+31 42 123 4567", {'type': 'string', 'format': 'phone'}).valid.should.be.true; 148 | }); 149 | 150 | it('should not validate an invalid phone-number', function () { 151 | this.validator.validate("31 42 123 4567", {'type': 'string', 'format': 'phone'}).valid.should.be.false; 152 | }); 153 | }); 154 | 155 | describe('uri', function () { 156 | it('should validate http://www.google.com/', function () { 157 | this.validator.validate("http://www.google.com/", {'type': 'string', 'format': 'uri'}).valid.should.be.true; 158 | }); 159 | 160 | it('should validate http://www.google.com/search', function () { 161 | this.validator.validate("http://www.google.com/search", {'type': 'string', 'format': 'uri'}).valid.should.be.true; 162 | }); 163 | 164 | it('should not validate relative URIs', function () { 165 | this.validator.validate("tdegrunt", {'type': 'string', 'format': 'uri'}).valid.should.be.false; 166 | }); 167 | 168 | it('should not validate with whitespace', function () { 169 | this.validator.validate("The dog jumped", {'type': 'string', 'format': 'uri'}).valid.should.be.false; 170 | }); 171 | }); 172 | 173 | describe('email', function () { 174 | it('should validate obama@whitehouse.gov', function () { 175 | this.validator.validate("obama@whitehouse.gov", {'type': 'string', 'format': 'email'}).valid.should.be.true; 176 | }); 177 | 178 | it('should validate barack+obama@whitehouse.gov', function () { 179 | this.validator.validate("barack+obama@whitehouse.gov", {'type': 'string', 'format': 'email'}).valid.should.be.true; 180 | }); 181 | 182 | it('should not validate obama@', function () { 183 | this.validator.validate("obama@", {'type': 'string', 'format': 'email'}).valid.should.be.false; 184 | }); 185 | }); 186 | 187 | describe('ip-address', function () { 188 | it('should validate 192.168.0.1', function () { 189 | this.validator.validate("192.168.0.1", {'type': 'string', 'format': 'ip-address'}).valid.should.be.true; 190 | }); 191 | 192 | it('should validate 127.0.0.1', function () { 193 | this.validator.validate("127.0.0.1", {'type': 'string', 'format': 'ip-address'}).valid.should.be.true; 194 | }); 195 | 196 | it('should not validate 192.168.0', function () { 197 | this.validator.validate("192.168.0", {'type': 'string', 'format': 'ip-address'}).valid.should.be.false; 198 | }); 199 | 200 | it('should not validate 256.168.0', function () { 201 | this.validator.validate("256.168.0", {'type': 'string', 'format': 'ip-address'}).valid.should.be.false; 202 | }); 203 | }); 204 | 205 | describe('ipv6', function () { 206 | it('should validate fe80::1%lo0', function () { 207 | this.validator.validate("fe80::1%lo0", {'type': 'string', 'format': 'ipv6'}).valid.should.be.true; 208 | }); 209 | 210 | it('should validate ::1', function () { 211 | this.validator.validate("::1", {'type': 'string', 'format': 'ipv6'}).valid.should.be.true; 212 | }); 213 | 214 | it('should not validate 127.0.0.1', function () { 215 | this.validator.validate("127.0.0.1", {'type': 'string', 'format': 'ipv6'}).valid.should.be.false; 216 | }); 217 | 218 | it('should not validate localhost', function () { 219 | this.validator.validate("localhost", {'type': 'string', 'format': 'ipv6'}).valid.should.be.false; 220 | }); 221 | 222 | }); 223 | 224 | describe('host-name', function () { 225 | it('should validate localhost', function () { 226 | this.validator.validate("localhost", {'type': 'string', 'format': 'host-name'}).valid.should.be.true; 227 | }); 228 | 229 | it('should validate www.google.com', function () { 230 | this.validator.validate("www.google.com", {'type': 'string', 'format': 'host-name'}).valid.should.be.true; 231 | }); 232 | 233 | it('should not validate www.-hi-.com', function () { 234 | this.validator.validate("www.-hi-.com", {'type': 'string', 'format': 'host-name'}).valid.should.be.false; 235 | }); 236 | }); 237 | 238 | 239 | describe('alpha', function () { 240 | it('should validate alpha', function () { 241 | this.validator.validate("alpha", {'type': 'string', 'format': 'alpha'}).valid.should.be.true; 242 | }); 243 | 244 | it('should validate abracadabra', function () { 245 | this.validator.validate("abracadabra", {'type': 'string', 'format': 'alpha'}).valid.should.be.true; 246 | }); 247 | 248 | it('should not validate 1test', function () { 249 | this.validator.validate("www.-hi-.com", {'type': 'string', 'format': 'alpha'}).valid.should.be.false; 250 | }); 251 | }); 252 | 253 | describe('alphanumeric', function () { 254 | it('should validate alphanumeric', function () { 255 | this.validator.validate("alpha", {'type': 'string', 'format': 'alphanumeric'}).valid.should.be.true; 256 | }); 257 | 258 | it('should validate 123', function () { 259 | this.validator.validate("123", {'type': 'string', 'format': 'alphanumeric'}).valid.should.be.true; 260 | }); 261 | 262 | it('should validate abracadabra123', function () { 263 | this.validator.validate("abracadabra123", {'type': 'string', 'format': 'alphanumeric'}).valid.should.be.true; 264 | }); 265 | 266 | it('should not validate 1test!', function () { 267 | this.validator.validate("1test!", {'type': 'string', 'format': 'alphanumeric'}).valid.should.be.false; 268 | }); 269 | }); 270 | 271 | describe('custom formats', function() { 272 | beforeEach(function() { 273 | this.validator.customFormats.foo = function(input) { 274 | if (input === 'foo') { 275 | return true; 276 | } 277 | return false; 278 | }; 279 | 280 | this.validator.customFormats.float = function(input) { 281 | //console.log(input); 282 | return /^\d+(?:\.\d+)?$/.test(input); 283 | }; 284 | }); 285 | 286 | it('should validate input', function() { 287 | this.validator.validate("foo", {'type': 'string', 'format': 'foo'}).valid.should.be.true; 288 | }); 289 | 290 | it('should validate numeric input', function() { 291 | this.validator.validate(32.45, {'type': 'number', 'format': 'float'}).valid.should.be.true; 292 | }); 293 | 294 | it('should fail input that fails validation', function() { 295 | this.validator.validate("boo", {'type': 'string', 'format': 'foo'}).valid.should.be.false; 296 | }); 297 | 298 | it('should fail numeric input that fails validation', function() { 299 | this.validator.validate(NaN, {'type': 'number', 'format': 'float'}).valid.should.be.false; 300 | }); 301 | 302 | describe('assigned to validator instances', function() { 303 | var format; 304 | 305 | beforeEach(function() { 306 | format = function() {}; 307 | this.validator.customFormats.boo = format; 308 | }); 309 | 310 | it('should not be assigned to the Validator prototype', function() { 311 | (typeof Validator.prototype.customFormats.boo).should.equal('undefined'); 312 | }); 313 | }); 314 | 315 | describe('assigned to the Validator.prototype before validator instances are created', function() { 316 | var format; 317 | 318 | beforeEach(function() { 319 | format = function() {}; 320 | Validator.prototype.customFormats.boo = format; 321 | }); 322 | 323 | afterEach(function() { 324 | delete Validator.prototype.customFormats.boo; 325 | }); 326 | 327 | it('should be assigned to the instances', function() { 328 | ((new Validator()).customFormats.boo).should.be.a('function'); 329 | }); 330 | }); 331 | }); 332 | 333 | describe('with options.disableFormat === true', function() { 334 | it('should validate invalid formats', function() { 335 | this.validator.validate("2012-07-08", {'type': 'string', 'format': 'date-time'}, 336 | {disableFormat: true}).valid.should.be.true; 337 | }); 338 | }); 339 | 340 | describe('invalid format', function() { 341 | it('should validate', function () { 342 | this.validator.validate("url", {'type': 'string', 'format': 'url'}).valid.should.be.true; 343 | }); 344 | }); 345 | }); 346 | -------------------------------------------------------------------------------- /test/i18n.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | 8 | var should = require('chai').should(); 9 | 10 | describe('i18n', function () { 11 | beforeEach(function () { 12 | this.validator = new Validator(); 13 | }); 14 | 15 | describe('attributes', function () { 16 | beforeEach(function () { 17 | this.validator = new Validator(); 18 | }); 19 | 20 | describe('type', function () { 21 | 22 | describe('number', function () { 23 | 24 | it('should provide an error name', function () { 25 | this.validator.validate('not-number', {'type': 'number'}) 26 | .errors[0].name.should.equal('type'); 27 | }); 28 | 29 | it('should provide an error argument', function () { 30 | should.exist(this.validator.validate('not-number', {'type': 'number'}) 31 | .errors[0].argument); 32 | }); 33 | 34 | }); 35 | 36 | describe('required', function () { 37 | 38 | it('should provide an error name', function () { 39 | this.validator.validate(undefined, {'type': 'number', 'required': true}) 40 | .errors[0].name.should.equal('required'); 41 | }); 42 | 43 | }); 44 | 45 | describe('null', function () { 46 | 47 | it('should provide an error name', function () { 48 | this.validator.validate('0', {'type': 'null'}) 49 | .errors[0].name.should.equal('type'); 50 | }); 51 | 52 | }); 53 | 54 | describe('date', function () { 55 | 56 | it('should provide an error name', function () { 57 | this.validator.validate('0', {'type': 'date'}) 58 | .errors[0].name.should.equal('type'); 59 | }); 60 | 61 | it('should provide an error argument', function () { 62 | should.exist(this.validator.validate('0', {'type': 'date'}) 63 | .errors[0].argument); 64 | }); 65 | 66 | }); 67 | 68 | describe('integer', function () { 69 | 70 | it('should provide an error name', function () { 71 | this.validator.validate(0.25, {'type': 'integer'}) 72 | .errors[0].name.should.equal('type'); 73 | }); 74 | 75 | it('should provide an error argument', function () { 76 | should.exist(this.validator.validate(0.25, {'type': 'integer'}) 77 | .errors[0].argument); 78 | }); 79 | 80 | }); 81 | 82 | describe('boolean', function () { 83 | 84 | it('should provide an error name', function () { 85 | this.validator.validate('true', {'type': 'boolean'}) 86 | .errors[0].name.should.equal('type'); 87 | }); 88 | 89 | it('should provide an error argument', function () { 90 | should.exist(this.validator.validate('true', {'type': 'boolean'}) 91 | .errors[0].argument); 92 | }); 93 | 94 | }); 95 | 96 | describe('any', function () { 97 | 98 | //NOTE: Because any will let through everything, the custom message must go on the required attribute 99 | 100 | it('should provide an error name', function () { 101 | this.validator.validate(undefined, {'type': 'any', required: true}) 102 | .errors[0].name.should.equal('required'); 103 | }); 104 | 105 | }); 106 | }); 107 | 108 | describe('minimum', function () { 109 | 110 | it('should provide an error name', function () { 111 | this.validator.validate(1, {'type': 'number', 'minimum': 2}) 112 | .errors[0].name.should.equal('minimum'); 113 | }); 114 | 115 | it('should provide an error argument', function () { 116 | should.exist(this.validator.validate(1, {'type': 'number', 'minimum': 2}) 117 | .errors[0].argument); 118 | }); 119 | 120 | describe('exclusiveMinimum', function () { 121 | 122 | it('should provide an error name', function () { 123 | this.validator.validate(1, {'type': 'number', 'minimum': 1, 'exclusiveMinimum': true}) 124 | .errors[0].name.should.equal('minimum'); 125 | }); 126 | 127 | it('should provide an error argument', function () { 128 | should.exist(this.validator.validate(1, {'type': 'number', 'minimum': 1, 'exclusiveMinimum': true}) 129 | .errors[0].argument); 130 | }); 131 | 132 | }); 133 | 134 | }); 135 | 136 | describe('maximum', function () { 137 | 138 | it('should provide an error name', function () { 139 | this.validator.validate(3, {'type': 'number', 'maximum': 1}) 140 | .errors[0].name.should.equal('maximum'); 141 | }); 142 | 143 | it('should provide an error argument', function () { 144 | should.exist(this.validator.validate(3, {'type': 'number', 'maximum': 1}) 145 | .errors[0].argument); 146 | }); 147 | 148 | describe('exclusiveMaximum', function () { 149 | 150 | it('should provide an error name', function () { 151 | this.validator.validate(2, {'type': 'number', 'maximum': 1, 'exclusiveMaximum': true}) 152 | .errors[0].name.should.equal('maximum'); 153 | }); 154 | 155 | it('should provide an error argument', function () { 156 | should.exist(this.validator.validate(2, {'type': 'number', 'maximum': 1, 'exclusiveMaximum': true}) 157 | .errors[0].argument); 158 | }); 159 | 160 | }); 161 | 162 | }); 163 | 164 | describe('divisibleBy', function () { 165 | 166 | it('should provide an error name', function () { 167 | this.validator.validate(1, {'type': 'number', 'divisibleBy': 2}) 168 | .errors[0].name.should.equal('divisibleBy'); 169 | }); 170 | 171 | it('should provide an error argument', function () { 172 | should.exist(this.validator.validate(1, {'type': 'number', 'divisibleBy': 2}) 173 | .errors[0].argument); 174 | }); 175 | 176 | }); 177 | 178 | describe('pattern', function () { 179 | 180 | it('should provide an error name', function () { 181 | this.validator.validate('abac', {'type': 'string', 'pattern': 'ab+c'}) 182 | .errors[0].name.should.equal('pattern'); 183 | }); 184 | 185 | it('should provide an error argument', function () { 186 | should.exist(this.validator.validate('abac', {'type': 'string', 'pattern': 'ab+c'}) 187 | .errors[0].argument); 188 | }); 189 | 190 | }); 191 | 192 | describe('minLength', function () { 193 | 194 | it('should provide an error name', function () { 195 | this.validator.validate('abcde', {'type': 'string', 'minLength': 6}) 196 | .errors[0].name.should.equal('minLength'); 197 | }); 198 | 199 | it('should provide an error argument', function () { 200 | should.exist(this.validator.validate('abcde', {'type': 'string', 'minLength': 6}) 201 | .errors[0].argument); 202 | }); 203 | 204 | }); 205 | 206 | describe('maxLength', function () { 207 | 208 | it('should provide an error name', function () { 209 | this.validator.validate('abcde', {'type': 'string', 'maxLength': 4}) 210 | .errors[0].name.should.equal('maxLength'); 211 | }); 212 | 213 | it('should provide an error argument', function () { 214 | should.exist(this.validator.validate('abcde', {'type': 'string', 'maxLength': 4}) 215 | .errors[0].argument); 216 | }); 217 | 218 | }); 219 | 220 | describe('enum', function () { 221 | 222 | it('should provide an error name', function () { 223 | this.validator.validate('abcde', {'type': 'string', 'enum': ['abcdf', 'abcdd']}) 224 | .errors[0].name.should.equal('enum'); 225 | }); 226 | 227 | it('should provide an error argument', function () { 228 | should.exist(this.validator.validate('abcde', {'type': 'string', 'enum': ['abcdf', 'abcdd']}) 229 | .errors[0].argument); 230 | }); 231 | 232 | }); 233 | 234 | describe('not', function () { 235 | 236 | it('should provide an error name', function () { 237 | this.validator.validate([1], {'type': 'any', 'not':'array'}) 238 | .errors[0].name.should.equal('not'); 239 | }); 240 | 241 | it('should provide an error argument', function () { 242 | should.exist(this.validator.validate([1], {'type': 'any', 'not':'array'}) 243 | .errors[0].argument); 244 | }); 245 | 246 | it('should prohibit specified types', function () { 247 | this.validator.validate([1], {'type': 'any', 'not':'array'}).valid.should.be.false; 248 | }); 249 | }); 250 | 251 | describe('disallow', function () { 252 | 253 | //NOTE: 'disallow' is a depreciated alias for 'not', custom error message will always use 'not' field 254 | 255 | it('should provide an error name', function () { 256 | this.validator.validate([1], {'type': 'any', 'disallow':'array'}) 257 | .errors[0].name.should.equal('not'); 258 | }); 259 | 260 | it('should provide an error argument', function () { 261 | should.exist(this.validator.validate([1], {'type': 'any', 'disallow':'array'}) 262 | .errors[0].argument); 263 | }); 264 | 265 | it('should prohibit specified types', function () { 266 | this.validator.validate([1], {'type': 'any', 'disallow':'array'}).valid.should.be.false; 267 | }); 268 | }); 269 | 270 | describe('dependencies', function () { 271 | 272 | it('should provide an error name', function () { 273 | this.validator.validate({quux: 1, foo: 1}, {'dependencies': {'quux': ['foo', 'bar']}}) 274 | .errors[0].name.should.equal('dependencies'); 275 | }); 276 | 277 | it('should provide an error argument', function () { 278 | should.exist(this.validator.validate({quux: 1, foo: 1}, {'dependencies': {'quux': ['foo', 'bar']}}) 279 | .errors[0].argument); 280 | }); 281 | 282 | }); 283 | }); 284 | 285 | describe('Formats', function () { 286 | beforeEach(function () { 287 | this.validator = new Validator(); 288 | }); 289 | 290 | describe('date-time', function () { 291 | 292 | it('should provide an error name', function () { 293 | this.validator.validate("2012-07-08", {'type': 'string', 'format': 'date-time'}) 294 | .errors[0].name.should.equal('format'); 295 | }); 296 | 297 | it('should provide an error argument', function () { 298 | this.validator.validate("2012-07-08", {'type': 'string', 'format': 'date-time'}) 299 | .errors[0].argument.should.equal('date-time'); 300 | }); 301 | 302 | }); 303 | 304 | describe('date', function () { 305 | 306 | it('should provide an error name', function () { 307 | this.validator.validate("TEST2012-07-08", {'type': 'string', 'format': 'date'}) 308 | .errors[0].name.should.equal('format'); 309 | }); 310 | 311 | it('should provide an error argument', function () { 312 | this.validator.validate("TEST2012-07-08", {'type': 'string', 'format': 'date'}) 313 | .errors[0].argument.should.equal('date'); 314 | }); 315 | 316 | }); 317 | 318 | describe('time', function () { 319 | 320 | it('should provide an error name', function () { 321 | this.validator.validate("16:41:41.532Z", {'type': 'string', 'format': 'time'}) 322 | .errors[0].name.should.equal('format'); 323 | }); 324 | 325 | it('should provide an error argument', function () { 326 | this.validator.validate("16:41:41.532Z", {'type': 'string', 'format': 'time'}) 327 | .errors[0].argument.should.equal('time'); 328 | }); 329 | 330 | }); 331 | 332 | describe('utc-millisec', function () { 333 | 334 | it('should provide an error name', function () { 335 | this.validator.validate("16:41:41.532Z", {'type': 'string', 'format': 'utc-millisec'}) 336 | .errors[0].name.should.equal('format'); 337 | }); 338 | 339 | it('should provide an error argument', function () { 340 | this.validator.validate("16:41:41.532Z", {'type': 'string', 'format': 'utc-millisec'}) 341 | .errors[0].argument.should.equal('utc-millisec'); 342 | }); 343 | 344 | }); 345 | 346 | describe('regex', function () { 347 | 348 | it('should provide an error name', function () { 349 | this.validator.validate("/^(abc]/", {'type': 'string', 'format': 'regex'}) 350 | .errors[0].name.should.equal('format'); 351 | }); 352 | 353 | it('should provide an error argument', function () { 354 | this.validator.validate("/^(abc]/", {'type': 'string', 'format': 'regex'}) 355 | .errors[0].argument.should.equal('regex'); 356 | }); 357 | 358 | }); 359 | 360 | describe('color', function () { 361 | 362 | it('should provide an error name', function () { 363 | this.validator.validate("json", {'type': 'string', 'format': 'color'}) 364 | .errors[0].name.should.equal('format'); 365 | }); 366 | 367 | it('should provide an error argument', function () { 368 | this.validator.validate("json", {'type': 'string', 'format': 'color'}) 369 | .errors[0].argument.should.equal('color'); 370 | }); 371 | 372 | }); 373 | 374 | describe('style', function () { 375 | 376 | it('should provide an error name', function () { 377 | this.validator.validate("0", {'type': 'string', 'format': 'style'}) 378 | .errors[0].name.should.equal('format'); 379 | }); 380 | 381 | it('should provide an error argument', function () { 382 | this.validator.validate("0", {'type': 'string', 'format': 'style'}) 383 | .errors[0].argument.should.equal('style'); 384 | }); 385 | 386 | }); 387 | 388 | describe('phone', function () { 389 | 390 | it('should provide an error name', function () { 391 | this.validator.validate("31 42 123 4567", {'type': 'string', 'format': 'phone'}) 392 | .errors[0].name.should.equal('format'); 393 | }); 394 | 395 | it('should provide an error argument', function () { 396 | this.validator.validate("31 42 123 4567", {'type': 'string', 'format': 'phone'}) 397 | .errors[0].argument.should.equal('phone'); 398 | }); 399 | 400 | }); 401 | 402 | describe('uri', function () { 403 | 404 | it('should provide an error name', function () { 405 | this.validator.validate("tdegrunt", {'type': 'string', 'format': 'uri'}) 406 | .errors[0].name.should.equal('format'); 407 | }); 408 | 409 | it('should provide an error argument', function () { 410 | this.validator.validate("tdegrunt", {'type': 'string', 'format': 'uri'}) 411 | .errors[0].argument.should.equal('uri'); 412 | }); 413 | 414 | }); 415 | 416 | describe('email', function () { 417 | 418 | it('should provide an error name', function () { 419 | this.validator.validate("obama@", {'type': 'string', 'format': 'email'}) 420 | .errors[0].name.should.equal('format'); 421 | }); 422 | 423 | it('should provide an error argument', function () { 424 | this.validator.validate("obama@", {'type': 'string', 'format': 'email'}) 425 | .errors[0].argument.should.equal('email'); 426 | }); 427 | 428 | }); 429 | 430 | describe('ip-address', function () { 431 | 432 | it('should provide an error name', function () { 433 | this.validator.validate("192.168.0", {'type': 'string', 'format': 'ip-address'}) 434 | .errors[0].name.should.equal('format'); 435 | }); 436 | 437 | it('should provide an error argument', function () { 438 | this.validator.validate("192.168.0", {'type': 'string', 'format': 'ip-address'}) 439 | .errors[0].argument.should.equal('ip-address'); 440 | }); 441 | 442 | }); 443 | 444 | describe('ipv6', function () { 445 | 446 | it('should provide an error name', function () { 447 | this.validator.validate("127.0.0.1", {'type': 'string', 'format': 'ipv6'}) 448 | .errors[0].name.should.equal('format'); 449 | }); 450 | 451 | it('should provide an error argument', function () { 452 | this.validator.validate("127.0.0.1", {'type': 'string', 'format': 'ipv6'}) 453 | .errors[0].argument.should.equal('ipv6'); 454 | }); 455 | 456 | }); 457 | 458 | describe('host-name', function () { 459 | 460 | it('should provide an error name', function () { 461 | this.validator.validate("www.-hi-.com", {'type': 'string', 'format': 'host-name'}) 462 | .errors[0].name.should.equal('format'); 463 | }); 464 | 465 | it('should provide an error argument', function () { 466 | this.validator.validate("www.-hi-.com", {'type': 'string', 'format': 'host-name'}) 467 | .errors[0].argument.should.equal('host-name'); 468 | }); 469 | 470 | }); 471 | 472 | 473 | describe('alpha', function () { 474 | 475 | it('should provide an error name', function () { 476 | this.validator.validate("www.-hi-.com", {'type': 'string', 'format': 'alpha'}) 477 | .errors[0].name.should.equal('format'); 478 | }); 479 | 480 | it('should provide an error argument', function () { 481 | this.validator.validate("www.-hi-.com", {'type': 'string', 'format': 'alpha'}) 482 | .errors[0].argument.should.equal('alpha'); 483 | }); 484 | 485 | }); 486 | 487 | describe('alphanumeric', function () { 488 | 489 | it('should provide an error name', function () { 490 | this.validator.validate("1test!", {'type': 'string', 'format': 'alphanumeric'}) 491 | .errors[0].name.should.equal('format'); 492 | }); 493 | 494 | it('should provide an error argument', function () { 495 | this.validator.validate("1test!", {'type': 'string', 'format': 'alphanumeric'}) 496 | .errors[0].argument.should.equal('alphanumeric'); 497 | }); 498 | 499 | }); 500 | }); 501 | 502 | describe('Arrays', function () { 503 | beforeEach(function () { 504 | this.validator = new Validator(); 505 | }); 506 | 507 | describe('simple array', function () { 508 | 509 | it('should provide an error name', function () { 510 | this.validator.validate(0, {'type': 'array'}) 511 | .errors[0].name.should.equal('type'); 512 | }); 513 | 514 | it('should provide an error argument', function () { 515 | should.exist(this.validator.validate(0, {'type': 'array'}) 516 | .errors[0].argument); 517 | }); 518 | 519 | describe('attribute on array items', function () { 520 | 521 | it('should provide an error name', function () { 522 | this.validator.validate(['1', '2', '3', 4], {'type': 'array', 'items': {'type': 'string'}}) 523 | .errors[0].name.should.equal('type'); 524 | }); 525 | 526 | it('should provide an error argument', function () { 527 | this.validator.validate(['1', '2', '3', '4$'], {'type': 'array', 'items': {'type': 'string', 'format': 'alphanumeric'}}) 528 | .errors[0].argument.should.equal('alphanumeric'); 529 | }); 530 | 531 | }); 532 | 533 | }); 534 | 535 | describe('minItems', function () { 536 | 537 | it('should provide an error name', function () { 538 | this.validator.validate([1], {'type': 'array', 'items': {'type': 'number'}, 'minItems': 2}) 539 | .errors[0].name.should.equal('minItems'); 540 | }); 541 | 542 | it('should provide an error argument', function () { 543 | should.exist(this.validator.validate([1], {'type': 'array', 'items': {'type': 'number'}, 'minItems': 2}) 544 | .errors[0].argument); 545 | }); 546 | 547 | }); 548 | 549 | describe('maxItems', function () { 550 | 551 | it('should provide an error name', function () { 552 | this.validator.validate([1, 2, 3], {'type': 'array', 'items': {'type': 'number'}, 'maxItems': 2}) 553 | .errors[0].name.should.equal('maxItems'); 554 | }); 555 | 556 | it('should provide an error argument', function () { 557 | should.exist(this.validator.validate([1, 2, 3], {'type': 'array', 'items': {'type': 'number'}, 'maxItems': 2}) 558 | .errors[0].argument); 559 | }); 560 | 561 | }); 562 | 563 | describe('uniqueItems', function () { 564 | 565 | it('should provide an error name', function () { 566 | this.validator.validate([1, 2, 4, 1, 3, 5], {'type': 'array', 'uniqueItems': true}) 567 | .errors[0].name.should.equal('uniqueItems'); 568 | }); 569 | 570 | }); 571 | }); 572 | 573 | describe('Mixed', function () { 574 | beforeEach(function () { 575 | this.validator = new Validator(); 576 | this.mixedSchema = { 577 | 'type': 'object', 578 | 'properties': { 579 | 'name': {'type': 'string'}, 580 | 'lines': { 581 | 'type': 'array', 582 | 'items': {'type': 'string', 'format': 'alphanumeric'}, 583 | }, 584 | }, 585 | }; 586 | }); 587 | 588 | describe('simple object with array with invalid items', function () { 589 | 590 | it('should provide an error name', function () { 591 | this.validator.validate({'name':'test', 'lines': ['1$']},this.mixedSchema) 592 | .errors[0].name.should.equal('format'); 593 | }); 594 | 595 | it('should provide an error argument', function () { 596 | this.validator.validate({'name':'test', 'lines': ['1$']},this.mixedSchema) 597 | .errors[0].argument.should.equal('alphanumeric'); 598 | }); 599 | }); 600 | }); 601 | 602 | }); 603 | -------------------------------------------------------------------------------- /test/interface.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var lib = require('../lib/index.js'); 4 | 5 | var assert = require('assert'); 6 | 7 | describe('public interface', function(){ 8 | it('Validator', function(){ 9 | assert.strictEqual(typeof lib.Validator, 'function'); 10 | }); 11 | 12 | it('ValidatorResult', function(){ 13 | assert.strictEqual(typeof lib.ValidatorResult, 'function'); 14 | }); 15 | 16 | it('ValidationError', function(){ 17 | assert.strictEqual(typeof lib.ValidationError, 'function'); 18 | }); 19 | 20 | it('SchemaError', function(){ 21 | assert.strictEqual(typeof lib.SchemaError, 'function'); 22 | }); 23 | 24 | it('SchemaScanResult', function(){ 25 | assert.strictEqual(typeof lib.SchemaScanResult, 'function'); 26 | }); 27 | 28 | it('scan', function(){ 29 | assert.strictEqual(typeof lib.scan, 'function'); 30 | }); 31 | 32 | it('validate', function(){ 33 | assert.strictEqual(typeof lib.validate, 'function'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/loading.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | var assert = require('assert'); 8 | 9 | require('chai').should(); 10 | 11 | var dataSchema = require('./fixtures/data_schema.json'); 12 | var dataCollectionSchema = require('./fixtures/data_collection_schema.json'); 13 | var typesSchema = require('./fixtures/types.json'); 14 | var dataCollection = require('./fixtures/data_collection.json'); 15 | 16 | describe('Schema management', function testSchemaManagement() { 17 | beforeEach(function () { 18 | this.validator = new Validator(); 19 | }); 20 | 21 | describe('when adding more than one schema', function testAddingTwoSchemas() { 22 | it('the first shouldn\'t be removed if referenced by the second', function testForDestructiveRefs() { 23 | this.validator.addSchema(dataSchema, '/data_schema.json'); 24 | this.validator.addSchema(dataCollectionSchema, '/data_collection_schema.json'); 25 | this.validator.schemas.should.not.have.property('/data_schema.json', null); 26 | }); 27 | 28 | it('a schema shouldn\'t be added to unresolved refs if it exists', function checkUnresolvedRefs() { 29 | this.validator.addSchema(dataSchema, '/data_schema.json'); 30 | this.validator.addSchema(dataCollectionSchema, '/data_collection_schema.json'); 31 | this.validator.unresolvedRefs.indexOf('/data_schema.json').should.equal(-1); 32 | }); 33 | 34 | it('the second schema should be able to use a reference to the first', function testForCorrectValidation() { 35 | this.validator.addSchema(dataSchema, '/data_schema.json'); 36 | this.validator.addSchema(dataCollectionSchema, '/data_collection_schema.json'); 37 | this.validator.addSchema(typesSchema, '/types.json'); 38 | this.validator.validate(dataCollection, { $ref: '/data_collection_schema.json' }).valid.should.be.true; 39 | }); 40 | 41 | it('Multiple schemas with the same id should throw an error', function testForCorrectValidation() { 42 | var validator = this.validator; 43 | assert.throws(function(){ 44 | validator.addSchema({ 45 | properties: { 46 | a: { id: 'http://example.com/schema.json', type: 'string' }, 47 | b: { $id: 'http://example.com/schema.json', type: 'number' }, 48 | }, 49 | }); 50 | }, function(err){ 51 | // This may be changed as necessary 52 | err.message.should.equal('Schema already exists with different definition'); 53 | return true; 54 | }) 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/metaschema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | 8 | var metaschema = require('json-metaschema/draft-04-schema.json'); 9 | 10 | require('chai').should(); 11 | 12 | describe('Meta-schema', function () { 13 | 14 | beforeEach(function () { 15 | this.validator = new Validator(); 16 | }); 17 | 18 | it('validates itself', function () { 19 | metaschema.should.exist; 20 | this.validator.validate(metaschema, metaschema).valid.should.be.true; 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/mixed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | 8 | require('chai').should(); 9 | 10 | describe('Mixed', function () { 11 | beforeEach(function () { 12 | this.validator = new Validator(); 13 | }); 14 | 15 | describe('simple object with array', function () { 16 | it('should validate', function () { 17 | this.validator.validate( 18 | {'name':'test', 'lines': ['1']}, 19 | { 20 | 'type': 'object', 21 | 'properties': { 22 | 'name': {'type': 'string'}, 23 | 'lines': { 24 | 'type': 'array', 25 | 'items': {'type': 'string'}, 26 | }, 27 | }, 28 | } 29 | ).valid.should.be.true; 30 | }); 31 | }); 32 | 33 | describe('simple object with array with invalid items', function () { 34 | it('should not validate', function () { 35 | var result = this.validator.validate( 36 | {'name':'test', 'lines': [1]}, 37 | { 38 | 'type': 'object', 39 | 'properties': { 40 | 'name': {'type': 'string'}, 41 | 'lines': { 42 | 'type': 'array', 43 | 'items': {'type': 'string'}, 44 | }, 45 | }, 46 | } 47 | ); 48 | result.errors.should.have.length(1); 49 | result.errors[0].should.have.property('message', 'is not of a type(s) string'); 50 | result.errors[0].should.have.property('property', 'instance.lines[0]'); 51 | }); 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /test/objects.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | var SchemaError = require('../lib/index.js').SchemaError; 8 | var assert = require('assert'); 9 | 10 | require('chai').should(); 11 | 12 | describe('Objects', function () { 13 | beforeEach(function () { 14 | this.validator = new Validator(); 15 | }); 16 | 17 | describe('simple object', function () { 18 | it('should validate a valid object', function () { 19 | this.validator.validate({}, {'type': 'object'}).valid.should.be.true; 20 | }); 21 | 22 | it('should validate an undefined object', function () { 23 | this.validator.validate(undefined, {'type': 'object'}).valid.should.be.true; 24 | }); 25 | 26 | it('should not validate a number', function () { 27 | return this.validator.validate(0, {'type': 'object'}).valid.should.be.false; 28 | }); 29 | 30 | it('should not validate an array', function () { 31 | return this.validator.validate([0], {'type': 'object'}).valid.should.be.false; 32 | }); 33 | }); 34 | 35 | describe('object with property', function () { 36 | it('should validate a valid object', function () { 37 | this.validator.validate( 38 | {'name': 'test'}, 39 | { 40 | 'type': 'object', 41 | 'properties': { 42 | 'name': {'type': 'string'}, 43 | }, 44 | } 45 | ).valid.should.be.true; 46 | }); 47 | 48 | it('should not validate an invalid object', function () { 49 | return this.validator.validate(0, {'type': 'object'}).valid.should.be.false; 50 | }); 51 | }); 52 | 53 | describe('object with enumerable properties in prototype chain', function () { 54 | var schema = { 55 | required: ['constructor'], 56 | properties: { 57 | constructor: { type: 'string' }, 58 | }, 59 | }; 60 | it('should validate a valid property', function () { 61 | var res = this.validator.validate(Object.create({constructor: 'string'}), schema); 62 | assert.strictEqual(res.valid, true); 63 | }); 64 | it('should not validate an invalid property', function () { 65 | var res = this.validator.validate(Object.create({constructor: true}), schema); 66 | assert.strictEqual(res.valid, false); 67 | res.errors[0].name.should.equal('type'); 68 | }); 69 | it('should not validate a missing property', function () { 70 | var res = this.validator.validate(Object.create({}), schema); 71 | assert.strictEqual(res.valid, false); 72 | res.errors[0].name.should.equal('required'); 73 | }); 74 | }); 75 | 76 | describe('object with properties', function () { 77 | it('should validate a valid object with multiple properties', function () { 78 | this.validator.validate( 79 | {'name': 'test', 'address': 'someplace'}, 80 | { 81 | 'type': 'object', 82 | 'properties': { 83 | 'name': {'type': 'string'}, 84 | 'address': {'type': 'string'}, 85 | }, 86 | } 87 | ).valid.should.be.true; 88 | }); 89 | 90 | it('should validate a valid object with undefined property', function () { 91 | this.validator.validate( 92 | {'name': 'test'}, 93 | { 94 | 'type': 'object', 95 | 'properties': { 96 | 'name': {'type': 'string'}, 97 | 'address': {'type': 'string'}, 98 | }, 99 | } 100 | ).valid.should.be.true; 101 | }); 102 | 103 | it('should not throw when checking properties on a non-object', function() { 104 | this.validator.validate( 105 | null, 106 | { 107 | 'type': 'object', 108 | 'properties': { 109 | 'name': {'type': 'string'}, 110 | }, 111 | } 112 | ).valid.should.be.false; 113 | }); 114 | 115 | }); 116 | 117 | describe('nested object with property', function () { 118 | it('should NOT validate a valid object', function () { 119 | this.validator.validate( 120 | {'name': 'test', 'nested': 'test2'}, 121 | { 122 | 'type': 'object', 123 | 'properties': { 124 | 'name': {'type': 'string'}, 125 | 'nested': {'type': 'object'}, 126 | }, 127 | } 128 | ).valid.should.be.false; 129 | }); 130 | 131 | it('should validate a valid object', function () { 132 | this.validator.validate( 133 | {'name': 'test', 'nested': 'test2'}, 134 | { 135 | 'type': 'object', 136 | 'properties': { 137 | 'name': {'type': 'string'}, 138 | 'nested': {'type': 'string'}, 139 | }, 140 | } 141 | ).valid.should.be.true; 142 | }); 143 | }); 144 | 145 | describe('undefined but required object', function () { 146 | it('should NOT validate an undefined object', function () { 147 | var ret = this.validator.validate( 148 | {'foo': {'baz': 1}}, 149 | { 150 | 'type': 'object', 151 | 'required': true, 152 | 'properties': { 153 | 'foo': { 154 | 'type': 'object', 155 | 'required': true, 156 | 'properties': { 157 | 'bar': {'type': 'object', 'required': true}, 158 | 'baz': {'type': 'number', 'required': true}, 159 | }, 160 | }, 161 | }, 162 | } 163 | ).valid.should.be.false; 164 | }); 165 | }); 166 | 167 | describe('additionalProperties', function () { 168 | it('should validate if there are no additionalProperties', function () { 169 | this.validator.validate( 170 | {'name': 'test', 'nested': 'test2'}, 171 | { 172 | 'type': 'object', 173 | 'properties': { 174 | 'name': {'type': 'string'}, 175 | 'nested': {'type': 'string'}, 176 | }, 177 | 'additionalProperties': false, 178 | } 179 | ).valid.should.be.true; 180 | }); 181 | 182 | it('should not validate if there are additionalProperties', function () { 183 | this.validator.validate( 184 | {'name': 'test', 'nested': 'test2', 'extraProp': 1}, 185 | { 186 | 'type': 'object', 187 | 'properties': { 188 | 'name': {'type': 'string'}, 189 | 'nested': {'type': 'string'}, 190 | }, 191 | 'additionalProperties': false, 192 | } 193 | ).valid.should.be.false; 194 | }); 195 | 196 | it('should validate if the additionalProperties are compliant with additionalProperties', function () { 197 | this.validator.validate( 198 | {'name': 'test', 'nested': 'test2', 'extraProp': 1}, 199 | { 200 | 'type': 'object', 201 | 'properties': { 202 | 'name': {'type': 'string'}, 203 | 'nested': {'type': 'string'}, 204 | }, 205 | 'additionalProperties': {'type': 'number'}, 206 | } 207 | ).valid.should.be.true; 208 | }); 209 | 210 | it('should not validate if the additionalProperties are not compliant with additionalProperties', function () { 211 | this.validator.validate( 212 | {'name': 'test', 'nested': 'test2', 'extraProp': '1'}, 213 | { 214 | 'type': 'object', 215 | 'properties': { 216 | 'name': {'type': 'string'}, 217 | 'nested': {'type': 'string'}, 218 | }, 219 | 'additionalProperties': {'type': 'number'}, 220 | } 221 | ).valid.should.be.false; 222 | }); 223 | 224 | }); 225 | 226 | describe('properties', function () { 227 | it('should treat undefined property schema as not existing', function () { 228 | var schema = { 229 | 'type': 'object', 230 | 'properties': { 231 | 'name': {'type': 'string'}, 232 | 'nested': undefined, 233 | }, 234 | 'additionalProperties': {'type': 'number'}, 235 | }; 236 | this.validator.validate( 237 | {'name': 'test', 'nested': 2}, 238 | schema 239 | ).valid.should.be.true; 240 | this.validator.validate( 241 | {'name': 'test', 'nested': 'test2'}, 242 | schema 243 | ).valid.should.be.false; 244 | }); 245 | 246 | it('should not permit null as a schema', function () { 247 | var validator = this.validator; 248 | var schema = { 249 | 'type': 'object', 250 | 'properties': { 251 | 'name': {'type': 'string'}, 252 | 'nested': null, 253 | }, 254 | 'additionalProperties': {'type': 'number'}, 255 | }; 256 | assert.throws(function(){ 257 | validator.validate( 258 | {'name': 'test', 'nested': 2}, 259 | schema 260 | ); 261 | }, function(err){ 262 | assert(err instanceof SchemaError); 263 | assert.strictEqual(err.message, 'Unexpected null, expected schema in "properties"'); 264 | return true; 265 | }); 266 | }); 267 | }); 268 | 269 | describe('patternProperties', function () { 270 | it('should treat undefined property schema as not existing', function () { 271 | var schema = { 272 | 'type': 'object', 273 | 'patternProperties': { 274 | 'name': {'type': 'string'}, 275 | 'nested': undefined, 276 | }, 277 | 'additionalProperties': {'type': 'number'}, 278 | }; 279 | this.validator.validate( 280 | {'name': 'test', 'nested': 2}, 281 | schema 282 | ).valid.should.be.true; 283 | this.validator.validate( 284 | {'name': 'test', 'nested': 'test2'}, 285 | schema 286 | ).valid.should.be.false; 287 | }); 288 | 289 | it('should not permit null as a schema', function () { 290 | var validator = this.validator; 291 | var schema = { 292 | 'type': 'object', 293 | 'patternProperties': { 294 | 'name': {'type': 'string'}, 295 | 'nested': null, 296 | }, 297 | 'additionalProperties': {'type': 'number'}, 298 | }; 299 | assert.throws(function(){ 300 | validator.validate( 301 | {'name': 'test', 'nested': 2}, 302 | schema 303 | ); 304 | }, function(err){ 305 | assert(err instanceof SchemaError); 306 | assert.strictEqual(err.message, 'Unexpected null, expected schema in "patternProperties"'); 307 | return true; 308 | }); 309 | }); 310 | }); 311 | }); 312 | -------------------------------------------------------------------------------- /test/pure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | var should = require('chai').should(); 8 | 9 | describe('API', function () { 10 | describe('arguments are pure', function () { 11 | // Pure meaning they aren't modified by the function call 12 | beforeEach(function () { 13 | this.validator = new Validator(); 14 | }); 15 | 16 | it('"dependencies" constraint', function () { 17 | var data = { 18 | "foo": [1,2,3], 19 | "bar": 2, 20 | }; 21 | var schema = { 22 | "dependencies": { 23 | "bar": { 24 | "properties": { 25 | "foo": { 26 | "type": "array", 27 | "items": {"type":"integer"}, 28 | }, 29 | "bar": {"type": "integer"}, 30 | }, 31 | "required": ["foo", "bar"], 32 | }, 33 | }, 34 | }; 35 | Object.freeze(data.foo); 36 | Object.freeze(data); 37 | this.validator.validate(data, schema); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/required_with_ref.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var util = require('util'); 7 | var fs = require('fs'); 8 | var assert = require('chai').assert; 9 | var Validator = require('../lib/index.js').Validator; 10 | 11 | /** 12 | * sort of functional test for "extends" and "required" 13 | */ 14 | describe('required with $ref', function () { 15 | var json, validator, schema, data, payment; 16 | 17 | beforeEach(function () { 18 | if(!json) { 19 | json = { 20 | schema :fs.readFileSync('test/fixtures/data_schema.json') 21 | ,types :fs.readFileSync('test/fixtures/types.json') 22 | ,data :fs.readFileSync('test/fixtures/data.json'), 23 | }; 24 | } 25 | validator = new Validator(); 26 | schema = JSON.parse(json.schema); 27 | validator.addSchema(JSON.parse(json.types), '/types.json'); 28 | data = JSON.parse(json.data); 29 | payment = data.payment; 30 | }); 31 | 32 | function assertValid(p) { 33 | validate(p, true); 34 | } 35 | function assertNotValid(p) { 36 | validate(p, false); 37 | } 38 | function validate(p, bool) { 39 | var result = validator.validate(p, schema); 40 | //console.log(util.inspect(validator,{depth: 3 })) 41 | assert.strictEqual(result.valid, bool, util.inspect(result.errors, { showHidden: false, depth: 1 })); 42 | } 43 | 44 | describe('fixture', function () { 45 | it('should validate', function () { 46 | assertValid(data); 47 | }); 48 | it('with wrong root node schould not be valid', function(){ 49 | assertNotValid({wrong_root:payment}); 50 | }); 51 | }); 52 | 53 | describe('required positive integer (amount)', function() { 54 | describe('valid', function() { 55 | it('1', function () { 56 | payment.amount = 1; 57 | assertValid(data); 58 | }); 59 | it('1000000000', function () { 60 | payment.amount = 1000000000; 61 | assertValid(data); 62 | }); 63 | }); 64 | describe('not valid', function() { 65 | it('missing', function () { 66 | delete(payment.amount); 67 | assertNotValid(data); 68 | }); 69 | it('1.2', function () { 70 | payment.amount = 1.2; 71 | assertNotValid(data); 72 | }); 73 | it('0', function () { 74 | payment.amount = 0; 75 | assertNotValid(data); 76 | }); 77 | it('-1', function () { 78 | payment.amount = -1; 79 | assertNotValid(data); 80 | }); 81 | it('-1.2', function () { 82 | payment.amount = -1.2; 83 | assertNotValid(data); 84 | }); 85 | it('foo', function () { 86 | payment.amount = 'foo'; 87 | assertNotValid(data); 88 | }); 89 | }); 90 | }); 91 | 92 | 93 | describe('required positive integer via $ref (other_amount)', function() { 94 | describe('valid', function() { 95 | it('1', function () { 96 | payment.other_amount = 1; 97 | assertValid(data); 98 | }); 99 | it('1000000000', function () { 100 | payment.other_amount = 1000000000; 101 | assertValid(data); 102 | }); 103 | }); 104 | describe('not valid', function() { 105 | it('missing', function () { 106 | delete(payment.other_amount); 107 | assertNotValid(data); 108 | }); 109 | it('1.2', function () { 110 | payment.other_amount = 1.2; 111 | assertNotValid(data); 112 | }); 113 | it('0', function () { 114 | payment.other_amount = 0; 115 | assertNotValid(data); 116 | }); 117 | it('-1', function () { 118 | payment.other_amount = -1; 119 | assertNotValid(data); 120 | }); 121 | it('-1.2', function () { 122 | payment.other_amount = -1.2; 123 | assertNotValid(data); 124 | }); 125 | it('foo', function () { 126 | payment.other_amount = 'foo'; 127 | assertNotValid(data); 128 | }); 129 | }); 130 | }); 131 | 132 | describe('optional string 1..255 (usage)', function() { 133 | describe('valid', function() { 134 | it('missing', function () { 135 | delete(payment.usage); 136 | assertValid(data); 137 | }); 138 | it('string', function () { 139 | payment.usage = "the usage"; 140 | assertValid(data); 141 | }); 142 | it('"a"', function () { 143 | payment.usage = 'a'; 144 | assertValid(data); 145 | }); 146 | it('255 chars', function () { 147 | payment.usage = 148 | "bbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqq" 149 | +"bbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqq" 150 | +"bbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbzzzzz"; 151 | assertValid(data); 152 | }); 153 | }); 154 | describe('not valid', function() { 155 | it('1234', function () { 156 | payment.usage = 1234; 157 | assertNotValid(data); 158 | }); 159 | it('null', function () { 160 | payment.usage = null; 161 | assertNotValid(data); 162 | }); 163 | it('""', function () { 164 | payment.usage = ''; 165 | assertNotValid(data); 166 | }); 167 | it('256 chars', function () { 168 | payment.usage = 169 | "bbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqq" 170 | +"bbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqq" 171 | +"bbbbbbbbbbqqqqqqqqqqbbbbbbbbbbqqqqqqqqqqbbbbbbbbbbzzzzzX"; 172 | assertNotValid(data); 173 | }); 174 | }); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /test/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var fs = require('fs'); 5 | var should = require('chai').should(); 6 | 7 | var Validator = require('../lib/index.js').Validator; 8 | var schemas = [ 9 | require('json-metaschema/draft-03-schema.json'), 10 | require('json-metaschema/draft-03-hyper-schema.json'), 11 | require('json-metaschema/draft-04-schema.json'), 12 | require('json-metaschema/draft-04-hyper-schema.json'), 13 | require('json-metaschema/draft-06-schema.json'), 14 | require('json-metaschema/draft-06-hyper-schema.json'), 15 | require('json-metaschema/draft-07-schema.json'), 16 | require('json-metaschema/draft-07-hyper-schema.json'), 17 | ]; 18 | 19 | var root = __dirname+'/suite/tests/'; 20 | var paths = [ 21 | 'draft3', 'draft3/optional', 22 | 'draft4', 'draft4/optional', 23 | 'draft6', 'draft6/optional', 24 | 'draft7', 'draft7/optional', 25 | ]; 26 | var ignoredFiles = ['optional', 'format', 'zeroTerminatedFloats.json', 'refRemote.json', 'ecmascript-regex.json', 'content.json', 'bignum.json', 'jsregex.json']; 27 | var ignoredTests = [ 28 | // TODO fix these tests for the next major release 29 | 'additionalItems should not look in applicators/items defined in extends are not examined', 30 | 'additionalProperties should not look in applicators/properties defined in extends are not examined', 31 | ]; 32 | var suiteFiles = []; 33 | paths.forEach(function(path){ 34 | fs.readdirSync(root+path).filter(function(file){ 35 | if (ignoredFiles.indexOf(file) >= 0) return false; 36 | return true; 37 | }).forEach(function(file){ 38 | suiteFiles.push(path + '/' + file); 39 | }); 40 | }); 41 | suiteFiles.push('draft7/optional/format/uri-reference.json'); 42 | 43 | /** 44 | * Runs the JSON Schema Test Suite 45 | */ 46 | describe('JSON Schema Test Suite', function(){ 47 | suiteFiles.forEach(function(filepath) { 48 | var suites = JSON.parse(fs.readFileSync(root+filepath)); 49 | suites.forEach(function(suite) { 50 | 51 | describe(filepath + ' ' + suite.description, function() { 52 | 53 | suite.tests.forEach(function(test) { 54 | 55 | it(test.description, function() { 56 | if(ignoredTests.indexOf(suite.description + '/' + test.description) >= 0) return void this.skip(); 57 | var validator = new Validator(); 58 | schemas.forEach(function(s){ validator.addSchema(s); }); 59 | var result = validator.validate(test.data, suite.schema); 60 | return should.equal(test.valid, result.valid, util.inspect(result, true, null)); 61 | }); 62 | 63 | }); 64 | }); 65 | 66 | }); 67 | 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/union.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jsl predef:define*/ 4 | /*jsl predef:it*/ 5 | 6 | var Validator = require('../lib/index.js').Validator; 7 | var should = require('chai').should(); 8 | 9 | describe('Union', function () { 10 | beforeEach(function () { 11 | this.validator = new Validator(); 12 | var testSchema = { 13 | "type" : "string", 14 | }; 15 | 16 | var numSchema = { 17 | "type" : "number", 18 | }; 19 | 20 | var objectIdSchema = { 21 | "type": "object", 22 | "id": "MongoDb#ObjectId", 23 | "description": "MongoDB ObjectID", 24 | "properties": { 25 | "id": {"type": "string"}, 26 | "_bsontype": {"type": "string"}, 27 | }, 28 | }; 29 | 30 | this.validator.addSchema(testSchema, '/Test#Simple'); 31 | this.validator.addSchema(numSchema, '/Test#Num'); 32 | this.validator.addSchema(objectIdSchema, '/MongoDb#ObjectId'); 33 | 34 | }); 35 | 36 | describe('string and number', function () { 37 | it('should validate for number', function () { 38 | this.validator.validate(1, {'type': ['number', 'string']}).valid.should.be.true; 39 | }); 40 | it('should validate for string', function () { 41 | this.validator.validate('1', {'type': ['number', 'string']}).valid.should.be.true; 42 | }); 43 | 44 | it('should not validate if no string or number', function () { 45 | this.validator.validate(true, {'type': ['number', 'string']}).valid.should.be.false; 46 | }); 47 | }); 48 | 49 | describe('string and null', function () { 50 | it('should validate for null', function () { 51 | this.validator.validate(null, {'type': ['null', 'string']}).valid.should.be.true; 52 | }); 53 | it('should validate for string', function () { 54 | this.validator.validate('1', {'type': ['null', 'string']}).valid.should.be.true; 55 | }); 56 | 57 | it('should not validate if no string or number', function () { 58 | this.validator.validate(true, {'type': ['null', 'string']}).valid.should.be.false; 59 | }); 60 | }); 61 | 62 | describe('string and null', function () { 63 | it('should validate for null', function () { 64 | this.validator.validate(null, {'type': ['null', 'string']}).valid.should.be.true; 65 | }); 66 | it('should validate for string', function () { 67 | this.validator.validate('1', {'type': ['null', 'string']}).valid.should.be.true; 68 | }); 69 | 70 | it('should not validate if no string or number', function () { 71 | this.validator.validate(true, {'type': ['null', 'string']}).valid.should.be.false; 72 | }); 73 | }); 74 | 75 | describe('null and $ref', function () { 76 | it('should validate for null', function () { 77 | this.validator.validate(null, {'type': ['null', {'$ref':'Test#Simple'}]}).valid.should.be.true; 78 | }); 79 | 80 | it('should validate for string', function () { 81 | this.validator.validate('test', {'type': ['null', {'$ref':'Test#Simple'}]}).valid.should.be.true; 82 | }); 83 | 84 | it('should not validate if no string or number', function () { 85 | this.validator.validate(true, {'type': ['null', {'$ref':'Test#Simple'}]}).valid.should.be.false; 86 | }); 87 | }); 88 | 89 | describe('$ref and string', function () { 90 | it('should validate for *', function () { 91 | this.validator.validate('*', {'type': [{ type: 'string', pattern: '^\\*$' }, {'$ref':'Test#Num'}]}).valid.should.be.true; 92 | }); 93 | 94 | it('should validate for 1', function () { 95 | this.validator.validate(1, {'type': [{ type: 'string', pattern: '^\\*$' }, {'$ref':'Test#Num'}]}).valid.should.be.true; 96 | }); 97 | 98 | it('should not validate for -', function () { 99 | var result = this.validator.validate('-', {'type': [{ type: 'string', pattern: '^\\*$' }, {'$ref':'Test#Num'}]}); 100 | result.valid.should.be.false; 101 | }); 102 | }); 103 | 104 | describe('complex $ref and string', function () { 105 | it('should validate for array or *', function () { 106 | var schema = { 107 | "type": "object", 108 | "properties": { 109 | "wildcards": { 110 | "type": "array", 111 | "items": {"type": [{"$ref": "MongoDb#ObjectId"}, {"type": "string", "pattern": "^\\*$"}]}, 112 | }, 113 | }, 114 | }; 115 | this.validator.validate({'wildcards': ['*']}, schema).valid.should.be.true; 116 | }); 117 | 118 | it('should validate for empty array', function () { 119 | var schema = { 120 | "type": "object", 121 | "properties": { 122 | "wildcards": { 123 | "type": "array", 124 | "items": {"type": [{"$ref": "MongoDb#ObjectId"}, {"type": "string", "pattern": "^\\*$"}]}, 125 | }, 126 | }, 127 | }; 128 | this.validator.validate({'wildcards': []}, schema).valid.should.be.true; 129 | }); 130 | 131 | it('should validate for objectid', function () { 132 | var schema = { 133 | "type": "object", 134 | "properties": { 135 | "wildcards": { 136 | "type": "array", 137 | "items": {"type": [{"$ref": "MongoDb#ObjectId"}, {"type": "string", "pattern": "^\\*$"}]}, 138 | }, 139 | }, 140 | }; 141 | this.validator.validate({'wildcards': [{"id": "1234", "_bsontype": "test"}, '*']}, schema).valid.should.be.true; 142 | }); 143 | 144 | it('should validate for objectid and ignore title and description', function () { 145 | var schema = { 146 | "type": "object", 147 | "properties": { 148 | "wildcards": { 149 | "type": "array", 150 | "items": {"type": [{"$ref": "MongoDb#ObjectId", "title": "test", "description": "test"}, {"type": "string", "pattern": "^\\*$"}]}, 151 | }, 152 | }, 153 | }; 154 | this.validator.validate({'wildcards': [{"id": "1234", "_bsontype": "test"}, '*']}, schema).valid.should.be.true; 155 | }); 156 | 157 | }); 158 | 159 | describe('union type in nested object array', function () { 160 | var schema = { 161 | type: 'object', 162 | required: true, 163 | properties: { 164 | frames: { 165 | type: 'array', 166 | required: true, 167 | items: { 168 | type: 'object', 169 | properties: { 170 | filename: {type: 'string', required: true}, 171 | lineno: {type: ['integer', 'null']}, 172 | method: {type: ['string', 'null']}, 173 | }, 174 | }, 175 | }, 176 | exception: { 177 | type: 'object', 178 | required: true, 179 | properties: { 180 | class: {type: 'string', required: true}, 181 | message: {type: 'string'}, 182 | }, 183 | }, 184 | }, 185 | }; 186 | var exc = {class: 'testing...', message: 'this is only a test'}; 187 | it('should validate for nulls', function () { 188 | var instance = {frames: [{filename: 'somefile.js', lineno: null}], exception: exc}; 189 | this.validator.validate(instance, schema).valid.should.be.true; 190 | }); 191 | it('should validate for null and string', function () { 192 | var instance = {frames: [{filename: 'somefile.js', lineno: null}], exception: exc}; 193 | this.validator.validate(instance, schema).valid.should.be.true; 194 | }); 195 | it('should not validate for string and string', function () { 196 | var instance = {frames: [{filename: 'somefile.js', lineno: {hello: 'world'}}], exception: exc}; 197 | this.validator.validate(instance, schema).valid.should.be.false; 198 | }); 199 | }); 200 | }); 201 | --------------------------------------------------------------------------------