├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── Release Notes.md ├── bin └── validate.js ├── examples ├── catalog │ ├── README.md │ ├── data.json │ ├── example.js │ └── productSet-schema.json └── fstab │ ├── README.md │ ├── data.json │ ├── entry-schema.json │ ├── example.js │ └── fstab-schema.json ├── lib ├── errors.js ├── httpLoader.js ├── jayschema.js ├── schemaRegistry.js ├── suites │ └── draft-04 │ │ ├── core.js │ │ ├── index.js │ │ ├── json-schema-draft-v4.json │ │ └── keywords │ │ ├── _propertiesImpl.js │ │ ├── additionalItems.js │ │ ├── additionalProperties.js │ │ ├── allOf.js │ │ ├── anyOf.js │ │ ├── dependencies.js │ │ ├── enum.js │ │ ├── exclusiveMaximum.js │ │ ├── exclusiveMinimum.js │ │ ├── format.js │ │ ├── formats │ │ ├── date-time.js │ │ ├── date.js │ │ ├── email.js │ │ ├── hostname.js │ │ ├── ipv4.js │ │ ├── ipv6.js │ │ ├── time.js │ │ └── uri.js │ │ ├── items.js │ │ ├── maxItems.js │ │ ├── maxLength.js │ │ ├── maxProperties.js │ │ ├── maximum.js │ │ ├── minItems.js │ │ ├── minLength.js │ │ ├── minProperties.js │ │ ├── minimum.js │ │ ├── multipleOf.js │ │ ├── not.js │ │ ├── oneOf.js │ │ ├── pattern.js │ │ ├── patternProperties.js │ │ ├── properties.js │ │ ├── required.js │ │ ├── type.js │ │ └── uniqueItems.js └── uri.js ├── package.json └── tests ├── apparentType.js ├── bootstrap.js ├── customFormatHandlers.js ├── customLoaders.js ├── downloadSchema.js ├── equality.js ├── errors.js ├── helpers.js ├── json-schema-test-suite-v3.js ├── json-schema-test-suite-v4.js ├── our-tests-async.js ├── our-tests-no-hasOwnProperty.js ├── our-tests.js ├── our-tests ├── arrays.json ├── format.json ├── numeric.json ├── objects.json ├── refs.json ├── refsAsync.json ├── refsSync.json ├── simpleTypes.json ├── strings.json ├── testThatApplyToAllTypes.json └── topLevelSchemas.json ├── refs.js ├── schemaRegistry.js ├── subSchemas.js └── uri.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | test.js 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/JSON-Schema-Test-Suite"] 2 | path = tests/JSON-Schema-Test-Suite 3 | url = git://github.com/json-schema/JSON-Schema-Test-Suite.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Nate Silva 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | * Neither the name of the author, nor the names of contributors may be used to 13 | endorse or promote products derived from this software without specific prior 14 | written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JaySchema 2 | 3 | ## Unmaintained 4 | 5 | :warning: This project is no longer maintained. This was one of the first validators to support JSON Schema v4, and to pass all of the tests that were available at that time. But today there are other options that use better parsing and are much faster, such as [ajv](https://github.com/epoberezkin/ajv), [is-my-json-valid](https://github.com/mafintosh/is-my-json-valid), and many more. If you’re happy with JaySchema, feel free to continue using it. If you’d like to take ownership of the project, contact me, but you’re probably better off switching to a modern alternative. 6 | 7 | ## About 8 | 9 | A [JSON Schema](http://json-schema.org/documentation.html) validator for Node.js. 10 | 11 | * Complete validation coverage of JSON Schema Draft v4. 12 | * Optional dynamic loader for referenced schemas (load schemas from a database or the web) 13 | * Useful error messages. 14 | * **NEW:** Supports custom validators for the `format` keword. 15 | 16 | ## Install 17 | 18 | npm install jayschema 19 | 20 | ## Usage 21 | 22 | ### CLI 23 | 24 | If you install the package globally (`npm install -g jayschema`), then you can directly invoke it from the command-line: 25 | 26 | ```shell 27 | $ jayschema my-schema.json 28 | ``` 29 | 30 | ### Basic usage 31 | 32 | ```js 33 | var JaySchema = require('jayschema'); 34 | var js = new JaySchema(); 35 | var instance = 64; 36 | var schema = { "type": "integer", "multipleOf": 8 }; 37 | 38 | // synchronous… 39 | console.log('synchronous result:', js.validate(instance, schema)); 40 | 41 | // …or async 42 | js.validate(instance, schema, function(errs) { 43 | if (errs) { console.error(errs); } 44 | else { console.log('async validation OK!'); } 45 | }); 46 | ``` 47 | 48 | ### Loading schemas from HTTP or from your database 49 | 50 | Here the Geographic Coordinate schema is loaded over HTTP. You can also supply your own loader—for example, to load schemas from a database. 51 | 52 | ```js 53 | var JaySchema = require('jayschema'); 54 | var js = new JaySchema(JaySchema.loaders.http); // we provide the HTTP loader here 55 | // you could load from a DB instead 56 | 57 | var instance = { "location": { "latitude": 48.8583, "longitude": 2.2945 } }; 58 | var schema = { 59 | "type": "object", 60 | "properties": { 61 | "location": { "$ref": "http://json-schema.org/geo" } 62 | } 63 | }; 64 | 65 | js.validate(instance, schema, function(errs) { 66 | if (errs) { console.error(errs); } 67 | else { console.log('validation OK!'); } 68 | }); 69 | ``` 70 | 71 | ### Custom format validators 72 | 73 | Create a custom validator for the JSON Schema `format` keyword: 74 | 75 | ```js 76 | var JaySchema = require('jayschema'); 77 | var js = new JaySchema(); 78 | 79 | js.addFormat('phone-us', function(value) { 80 | var PHONE_US_REGEXP = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/; 81 | if (PHONE_US_REGEXP.test(value)) { return null; } 82 | return 'must be a US phone number'; 83 | }); 84 | 85 | var instance = '212-555-'; 86 | var schema = { "type": "string", "format": "phone-us" }; 87 | 88 | console.log(js.validate(instance, schema)); 89 | // fails with error description: must be a US phone number 90 | ```` 91 | 92 | ## Why JSON Schema? 93 | 94 | * Validate JSON server-side: 95 | * For your JSON-based API 96 | * For data that you want to store in a NoSQL database 97 | * No [ORM](https://npmjs.org/browse/keyword/orm) required. Change databases or store data in multiple databases using the same schemas. For example, session data in Redis, permanent data in MongoDB. 98 | * JSON Schema has a really nice declarative syntax. See the [official examples](http://json-schema.org/examples.html). 99 | 100 | ## API 101 | 102 | ### JaySchema([loader]) 103 | 104 | **(Constructor)** The optional *loader* will be called each time an external `$ref` is encountered. It should load the referenced schema and return it. 105 | 106 | If you don’t reference any external schemas, or if you pre-register all the schemas you’re using, you don’t need to provide a *loader*. 107 | 108 | **If you provide a *loader*, you should call the validate() function asynchronously.** That’s because loading involves disk or network I/O, and I/O operations in Node are asynchronous. 109 | 110 | Sample loader skeleton: 111 | 112 | ```js 113 | function loader(ref, callback) { 114 | // ref is the schema to load 115 | // [ load your schema! ] 116 | if (errorOccurred) { 117 | callback(err); 118 | } else { 119 | callback(null, schema); 120 | } 121 | } 122 | ``` 123 | 124 | ### JaySchema.prototype.validate(instance, schema [, callback]) 125 | 126 | Validate a JSON object, *instance*, against the given *schema*. If you provide a *callback*, validation will be done asynchronously. 127 | 128 | *schema* can be an actual JSON Schema (a JSON object), or it can be the `id` of a previously-registered schema (a string). 129 | 130 | #### Return value 131 | 132 | * **async:** Uses the standard Node callback signature. The first argument will be an array of errors, if any errors occurred, or `undefined` on success. 133 | * **synchronous:** If you don’t provide a callback, an array of errors will be returned. Success is indicated by an empty array. 134 | 135 | ### JaySchema.prototype.register(schema [, id]) 136 | 137 | Manually register *schema*. Useful if you have several related schemas you are working with. The optional *id* can be used to register a schema that doesn’t have an `id` property, or which is referenced using a unique id. 138 | 139 | **Returns:** an array of missing schemas. A missing schema is one that was `$ref`erenced by the registered schema, but hasn’t been regisetered yet. If no missing schemas were referenced, an empty array is returned. 140 | 141 | See [Schema loading](#schema-loading). 142 | 143 | ### JaySchema.prototype.getMissingSchemas() 144 | 145 | Returns an array of missing schemas. A missing schema is one that was `$ref`erenced by a `register()`ed schema, but the referenced schema has not yet been loaded. 146 | 147 | See [Schema loading](#schema-loading). 148 | 149 | ### JaySchema.prototype.isRegistered(id) 150 | 151 | Return boolean indicating whether the specified schema id has previously been registered. 152 | 153 | See [Schema loading](#schema-loading). 154 | 155 | ### JaySchema.prototype.addFormat(formatName, handler) 156 | 157 | Add a custom handler for the `format` keyword. Whenever a schema uses the `format` keyword, with the given `formatName`, your handler will be called. 158 | 159 | The handler receives the value to be validated, and returns `null` if the value is valid, or a `string` (description of the error) if the value is not valid. 160 | 161 | ### Loaders 162 | 163 | A loader can be passed to the constructor, or you can set the `loader` property at any time. You can define your own loader. **JaySchema** also includes one built-in loader for your convenience: 164 | 165 | #### JaySchema.loaders.http 166 | 167 | Loads external `$ref`s using HTTP. :warning: **Caveat:** HTTP is inherently unreliable. For example, the network or site may be down, or the referenced schema may not be available any more. You really shouldn’t use this in production, but it’s great for testing. 168 | 169 | ### Configuration options 170 | 171 | ### maxRecursion 172 | 173 | The maximum depth to recurse when retrieving external `$ref` schemas using a loader. The default is `5`. 174 | 175 | ### loader 176 | 177 | The schema loader to use, if any. (The same schema loader that was passed to the `JaySchema` constructor.) You can change or override this at any time. 178 | 179 | ## Schema loading 180 | 181 | **JaySchema** provides several ways to register externally-referenced schemas. 182 | 183 | You use the `$ref` keyword to pull in an external schema. For example, you might reference a schema that’s available in a local database. 184 | 185 | Validation will fail if **JaySchema** encounters a validation rule that references an external schema, if that schema is not `register`ed. 186 | 187 | There are several ways to ensure that all referenced schemas are registered: 188 | 189 | ### Using a loader 190 | 191 | Pass a `loader` callback to the `JaySchema` constructor. When an external schema is needed, **JaySchema** will call your loader. See the constructor documentation, above. Using a loader requires you to validate asynchronously. 192 | 193 | ### By using the `getMissingSchemas()` method 194 | 195 | This works with synchronous or async code. 196 | 197 | 1. First, `register()` the main schemas you plan to use. 198 | 2. Next, call `getMissingSchemas`, which returns an array of externally-referenced schemas. 199 | 3. Retrieve and `register()` each missing schema. 200 | 4. Repeat from step 2 until there are no more missing schemas. 201 | 202 | ### By using the `register()` return value 203 | 204 | This works with synchronous or async code. 205 | 206 | Each time you call `register(schema)`, the return value will be an array of missing external schemas that were referenced. You can use this to register the missing schemas. 207 | 208 | In other words: calling `register(schemaA);` will (1) register `schemaA` and (2) return a list of missing schemas that were referenced by `schemaA`. 209 | 210 | If, instead, you want the list of *all* missing schemas referenced by all registrations that have been done so far, use the `getMissingSchemas()` method, above. 211 | 212 | ## Format specifiers 213 | 214 | For the `format` keyword, **JaySchema** allows you to add custom validation functions (see [`addFormat`](#jayschemaprototypeaddformatformatname-handler)). In addition, the following built-in formats are supported: 215 | 216 | * `date-time`: Must match the `date-time` specification given in [RFC 3339, Section 5.6](https://tools.ietf.org/html/rfc3339#section-5.6). This expects *both* a date and a time. For date-only validation or time-only validation, JaySchema supports the older draft v3 `date` and `time` formats. 217 | * `hostname`: Must match the “Preferred name syntax” given in [RFC 1034, Section 3.5](https://tools.ietf.org/html/rfc1034#section-3.5), with the exception that hostnames are permitted to begin with a digit, as per [RFC 1123 Section 2.1](http://tools.ietf.org/html/rfc1123#section-2.1). 218 | * `email`: Must match [RFC 5322, Section 3.4.1](https://tools.ietf.org/html/rfc5322#section-3.4.1), with the following limitations: `quoted-string`s, `domain-literal`s, comments, and folding whitespace are not supported; the `domain` portion must be a hostname as in the `hostname` keyword. 219 | * `ipv4`: Must be a dotted-quad IPv4 address. 220 | * `ipv6`: Must be a valid IPv6 address as per [RFC 2373 section 2.2](http://tools.ietf.org/html/rfc2373#section-2.2). 221 | * `uri`: As in [RFC 3986 Appendix A](http://tools.ietf.org/html/rfc3986#appendix-A), including relative URIs (no scheme part, fragment-only), with the exception that well-formedness of internal elements, including percent encoding and authority strings, is not verified. 222 | -------------------------------------------------------------------------------- /Release Notes.md: -------------------------------------------------------------------------------- 1 | # 0.3.1 2 | 3 | * **BUGFIX**: When anonymous sub-schemas were used with `oneOf` or `anyOf`, the error object for the sub-schemas was being overridden due to a variable scope problem. Thanks to [mrhooray](https://github.com/mrhooray). 4 | 5 | # 0.3.0 6 | 7 | * **FEATURE:** [Custom `format` validators](https://github.com/natesilva/jayschema#jayschemaprototypeaddformatformatname-handler) are now supported. The syntax is compatible with that of [tv4](https://github.com/geraintluff/tv4#addformatformat-validationfunction). Thanks to [alexkwolfe](https://github.com/alexkwolfe) for the suggestion. 8 | 9 | # 0.2.8 10 | 11 | * **BUGFIX**: `enum` properties with `null` values now work. Thanks to [alexkwolfe](https://github.com/alexkwolfe). 12 | 13 | # 0.2.7 14 | 15 | * **BUGFIX**: Handle falsy values correctly in `ValidationError`. Thanks to [larose](https://github.com/larose). 16 | * Other code cleanups. Thanks again to [larose](https://github.com/larose). 17 | 18 | # 0.2.6 19 | 20 | * **BUGFIX**: An `undefined` instance is no longer tested as if it’s an `object`. Fixes a crash that occurred if the instance was `undefined` and the schema used a `required` keyword. Thanks to [emschwartz](https://github.com/emschwartz). 21 | 22 | # 0.2.5 23 | 24 | * **BUGFIX**: Explicitly specifying an unrecognized top-level `$schema` (something other than JSON Schema Draft 4) now returns a standard JaySchema error instead of dumping you out to a stack trace. Thanks to [schimmy](https://github.com/schimmy). 25 | * **BUGFIX/ENHANCEMENT**: You can now specify the “current version” `$schema`. Previously the only schema you could explicitly specify was the Draft 4 schema. In other words, any the following at the top of a schema def will work: 26 | * `{ "$schema": "http://json-schema.org/draft-04/schema#" }` 27 | * this has always been supported 28 | * `{ "$schema": "http://json-schema.org/schema#"}` 29 | * this is new and will use the draft-04 schema rules 30 | * (no `$schema` value) 31 | * this has always been supported and will use the draft-04 schema rules 32 | 33 | Thanks to [schimmy](https://github.com/schimmy). 34 | 35 | # 0.2.4 36 | 37 | * **BUGFIX**: Schema ID URLs that have `..` in them are normalized and can now be used. Thanks to [alexbirkett](https://github.com/alexbirkett). 38 | 39 | * **BUGFIX**: Fixed a hang/timeout when a loader function is provided, but no external schemas are loaded. Thanks to [alexbirkett](https://github.com/alexbirkett). 40 | 41 | # 0.2.3 42 | 43 | * **BUGFIX**: Better logic for validating the `hostname` format. IPv4 addresses are no longer recognized as hostnames. Thanks to [k7sleeper](https://github.com/k7sleeper). 44 | 45 | # 0.2.2 46 | 47 | * **BUGFIX/ENHANCEMENT**: Instances and schemas which do not have the `Object` prototype are now supported. Thanks to [adrianlang](https://github.com/adrianlang). 48 | 49 | # 0.2.1 50 | 51 | * **BUGFIX/ENHANCEMENT**: Properly support the Draft v3 `date` and `time` formats. 52 | 53 | # 0.2.0 54 | 55 | * :exclamation: — **[BREAKING CHANGE]** In a few rare cases the async version of `validate()` returned a bare error object. The async version of `validate()` is documented to return an *array* of error objects, or `undefined` if there are no errors. It now behaves as documented. 56 | * There’s a small possibility that this could break code that depends on the old, quirky, behavior. Therefore the version number has been incremented so existing projects that depend on the 0.1.x behavior will not be unexpectedly upgraded, as long as you are following [best practices](https://npmjs.org/doc/json.html#Tilde-Version-Ranges) for `package.json` dependencies. 57 | * A good dependency specification is: `"jayschema": "~0.2.0"`, or `"jayschema": "~0.1.6"` if you still need the old behavior. 58 | * `"jayschema": "0.2.x"` and `"jayschema": "0.1.x"` would also work. 59 | 60 | # 0.1.6 61 | 62 | * **BUGFIX**: Fix a race condition when doing multiple simultaneous validations. Previously, if you were doing several validations and they all referred to the same external schema (that needs to be loaded using a loader function), some of the validations could fail. This has been fixed. 63 | * **BUGFIX**: Fix a potential failure to resolve a URI that affected Node > 0.9.1. 64 | * **ENHANCEMENT**: Improved compatibility with Node 0.6.x. Versions 0.6.6 and higher are supported. Previously the main library worked, but the tests and command-line utility would fail. 65 | * **CHANGE/BUGFIX**: The sample HTTP loader utility (`JaySchema.loaders.http`) was failing to load from HTTPS URLs on Node > 0.9.1. It can once again load from SSL sources, but note that it does not attempt to validate the SSL certificate. If you need more sophisticated HTTPS functionality, consider writing your own loader using something like [request](https://github.com/mikeal/request), which gives you much more control. 66 | 67 | # 0.1.5 68 | 69 | * **FEATURE**: You can now query to see if a schema has been registered, by calling the `isRegistered(id)` method of the `JaySchema` class. 70 | 71 | # 0.1.4 72 | 73 | * **BUGFIX**: More consistent checks for registered schemas. Fixes issue #2: the `register()` method return value was showing some schemas missing, when in fact, they were registered. 74 | 75 | # 0.1.3 76 | 77 | * **BUGFIX**: The `getMissingSchemas()` method is fixed. It was showing some schemas as missing, when in fact, they were registered. 78 | * **FEATURE**: You can pass a string instead of a schema to the `validate()` function. If the string is the `id` of a registered schema, your instance will be validated against that schema. 79 | 80 | # 0.1.2 81 | 82 | * The `JaySchema.errors` object is now exposed. Authors of schema loaders may wish to use the `JaySchema.errors.SchemaLoaderError` to signal failure to load a requested schema. 83 | * The included HTTP loader now works with HTTPS as well, and follows 3XX redirects. 84 | 85 | # 0.1.1 86 | 87 | * Nested `$ref`s which refer to other `$ref`s are now handled correctly. 88 | 89 | # 0.1.0 90 | 91 | * First non-beta release. 92 | 93 | # 0.1.0-beta.5 94 | 95 | * Updated test suite to include the new draft4 tests in JSON-Schema-Test-Suite. 96 | * Better internal handling of schema registration. 97 | * Fixed bugs related to using a URN schema id. 98 | * Fixed a bug affecting the "multipleOf" keyword when using very small numbers. 99 | 100 | # 0.1.0-beta.4 101 | 102 | * Improved performance and reduced memory usage. 103 | * Internal code organization clean-up. 104 | 105 | # 0.1.0-beta.3 106 | 107 | * Improved performance as indicated by profiling. 108 | 109 | # 0.1.0-beta 110 | 111 | * Updated version number to 0.1.0-beta, as 0.1.0 is the likely version number of the first non-beta release. 112 | * :new: — Support for the `format` keyword. All formats defined in the spec are supported: `date-time`, `email`, `hostname`, `ipv4`, `ipv6` and `uri`. 113 | 114 | # 0.0.1-beta 115 | 116 | * :exclamation: — **[breaking change]** The `validate()` method no longer auto-downloads schemas from HTTP. If you rely on this functionality, the following code is equivalent: 117 | * Old code (with auto HTTP loading): `var js = new JaySchema();` 118 | * New equivalent: `var js = new JaySchema(JaySchema.loaders.http);` 119 | * :new: — **[major feature]** Customizable loader for external schemas. You can provide a custom loader that will be called when an external schema is referenced. This allows you to reference schemas that are stored in a database, downloaded from HTTP, or by any other method you choose. 120 | * Upgraded to beta—no further breaking changes are planned for this release. 121 | 122 | # 0.0.1-alpha 123 | 124 | * :new: — Initial release 125 | -------------------------------------------------------------------------------- /bin/validate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // simple command-line validation 4 | 5 | var JaySchema = require('../lib/jayschema.js') 6 | , fs = require('fs') 7 | , path = require('path') 8 | ; 9 | 10 | // support Node 0.6.x 11 | var existsSync = fs.existsSync || path.existsSync; 12 | 13 | var META_SCHEMA_PATH = path.join(__dirname, '..', 'lib', 'suites', 'draft-04', 14 | 'json-schema-draft-v4.json'); 15 | var META_SCHEMA = require(META_SCHEMA_PATH); 16 | 17 | var instance = process.argv[2]; 18 | var schema = process.argv[3] || META_SCHEMA_PATH; 19 | 20 | var syntax = function() { 21 | console.log('Syntax: jayschema []'); 22 | console.log('\tif is omitted, the will be validated'); 23 | console.log('\tagainst the JSON Schema Draft v4 meta-schema'); 24 | }; 25 | 26 | if (!instance || !schema) { 27 | return syntax(); 28 | } 29 | 30 | if (!existsSync(instance)) { 31 | console.error('ERR: instance', '"' + instance + '"', 'not found'); 32 | return; 33 | } 34 | 35 | if (!existsSync(schema)) { 36 | console.error('ERR: schema', '"' + schema + '"', 'not found'); 37 | return; 38 | } 39 | 40 | var instanceRaw = fs.readFileSync(instance); 41 | var schemaRaw = fs.readFileSync(schema); 42 | 43 | try { 44 | var instanceJson = JSON.parse(instanceRaw); 45 | } catch (e) { 46 | console.error('ERR: instance is not valid JSON'); 47 | return; 48 | } 49 | 50 | try { 51 | var schemaJson = JSON.parse(schemaRaw); 52 | } catch (e) { 53 | console.error('ERR: schema is not valid JSON'); 54 | return; 55 | } 56 | 57 | var js = new JaySchema(); 58 | 59 | var schemaErrors = js.validate(schemaJson, META_SCHEMA); 60 | if (schemaErrors.length) { 61 | console.error('ERR: schema is not valid JSON Schema Draft v4'); 62 | console.log(require('util').inspect(schemaErrors, false, null)); 63 | return; 64 | } 65 | 66 | var result = js.validate(instanceJson, schemaJson); 67 | 68 | if (result.length === 0) { 69 | console.log('validation OK'); 70 | } else { 71 | console.log('validation errors:'); 72 | console.log(require('util').inspect(result, false, null)); 73 | } 74 | -------------------------------------------------------------------------------- /examples/catalog/README.md: -------------------------------------------------------------------------------- 1 | ## Catalog example 2 | 3 | Based on the [catalog example](http://json-schema.org/example1.html) from the JSON Schema web site. 4 | -------------------------------------------------------------------------------- /examples/catalog/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 2, 4 | "name": "An ice sculpture", 5 | "price": 12.50, 6 | "tags": ["cold", "ice"], 7 | "dimensions": { 8 | "length": 7.0, 9 | "width": 12.0, 10 | "height": 9.5 11 | }, 12 | "warehouseLocation": { 13 | "latitude": -78.75, 14 | "longitude": 20.4 15 | } 16 | }, 17 | { 18 | "id": 3, 19 | "name": "A blue mouse", 20 | "price": 25.50, 21 | "dimensions": { 22 | "length": 3.1, 23 | "width": 1.0, 24 | "height": 1.0 25 | }, 26 | "warehouseLocation": { 27 | "latitude": 54.4, 28 | "longitude": -32.7 29 | } 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /examples/catalog/example.js: -------------------------------------------------------------------------------- 1 | // 2 | // Example showing how to validate the "Product Set" schema shown in 3 | // http://json-schema.org/example1.html. 4 | // 5 | 6 | var JaySchema = require('../../lib/jayschema.js') 7 | ; 8 | 9 | // Load the schema and the data to be validated 10 | var schema = require('./productSet-schema.json'); 11 | var data = require('./data.json'); 12 | 13 | // Create the JaySchema object 14 | var js = new JaySchema(); 15 | 16 | // Register our schema 17 | // 18 | // If you are just using one schema, you don't have to do this, but 19 | // it's useful, because it will let us know about any missing 20 | // referenced schemas. 21 | 22 | var missingSchemas = js.register(schema); 23 | 24 | if (missingSchemas.length) { 25 | // It turns out the productSet schema references a remote schema, 26 | // with an id of "http://json-schema.org/geo". We have to load and 27 | // register that. 28 | // 29 | // We have a few of options. (A) We could load it here by any 30 | // means and register() it. (B) We could write our own loader 31 | // callback that will be called by JaySchema when it encounters a 32 | // missing schema. (C) Because this schema has an HTTP id, We can 33 | // do it the easy way and use JaySchema's built-in HTTP loader. 34 | // 35 | // Here is the easy way: 36 | js.loader = JaySchema.loaders.http; 37 | } 38 | 39 | // Okay, let's validate, asynchronously. 40 | js.validate(data, schema, function(errs) { 41 | if (errs) { console.error('validation errors:\n', errs); } 42 | else { console.log('no validation errors!'); } 43 | }); 44 | -------------------------------------------------------------------------------- /examples/catalog/productSet-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Product set", 4 | "type": "array", 5 | "items": { 6 | "title": "Product", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "description": "The unique identifier for a product", 11 | "type": "number" 12 | }, 13 | "name": { 14 | "type": "string" 15 | }, 16 | "price": { 17 | "type": "number", 18 | "minimum": 0, 19 | "exclusiveMinimum": true 20 | }, 21 | "tags": { 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | }, 26 | "minItems": 1, 27 | "uniqueItems": true 28 | }, 29 | "dimensions": { 30 | "type": "object", 31 | "properties": { 32 | "length": {"type": "number"}, 33 | "width": {"type": "number"}, 34 | "height": {"type": "number"} 35 | }, 36 | "required": ["length", "width", "height"] 37 | }, 38 | "warehouseLocation": { 39 | "description": "Coordinates of the warehouse with the product", 40 | "$ref": "http://json-schema.org/geo" 41 | } 42 | }, 43 | "required": ["id", "name", "price"] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/fstab/README.md: -------------------------------------------------------------------------------- 1 | ## Catalog example 2 | 3 | Based on the [“/etc/fstab” example](http://json-schema.org/example2.html) from the JSON Schema web site. 4 | -------------------------------------------------------------------------------- /examples/fstab/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": { 3 | "storage": { 4 | "type": "disk", 5 | "device": "/dev/sda1" 6 | }, 7 | "fstype": "btrfs", 8 | "readonly": true 9 | }, 10 | "/var": { 11 | "storage": { 12 | "type": "disk", 13 | "label": "8f3ba6f4-5c70-46ec-83af-0d5434953e5f" 14 | }, 15 | "fstype": "ext4", 16 | "options": [ "nosuid" ] 17 | }, 18 | "/tmp": { 19 | "storage": { 20 | "type": "tmpfs", 21 | "sizeInMB": 64 22 | } 23 | }, 24 | "/var/www": { 25 | "storage": { 26 | "type": "nfs", 27 | "server": "my.nfs.server", 28 | "remotePath": "/exports/mypath" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/fstab/entry-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://some.site.somewhere/entry-schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "schema for an fstab entry", 5 | "type": "object", 6 | "required": [ "storage" ], 7 | "properties": { 8 | "storage": { 9 | "type": "object", 10 | "oneOf": [ 11 | { "$ref": "#/definitions/diskDevice" }, 12 | { "$ref": "#/definitions/diskUUID" }, 13 | { "$ref": "#/definitions/nfs" }, 14 | { "$ref": "#/definitions/tmpfs" } 15 | ] 16 | }, 17 | "fstype": { 18 | "enum": [ "ext3", "ext4", "btrfs" ] 19 | }, 20 | "options": { 21 | "type": "array", 22 | "minItems": 1, 23 | "items": { "type": "string" }, 24 | "uniqueItems": true 25 | }, 26 | "readonly": { "type": "boolean" } 27 | }, 28 | "definitions": { 29 | "diskDevice": { 30 | "properties": { 31 | "type": { "enum": [ "disk" ] }, 32 | "device": { 33 | "type": "string", 34 | "pattern": "^/dev/[^/]+(/[^/]+)*$" 35 | } 36 | }, 37 | "required": [ "type", "device" ], 38 | "additionalProperties": false 39 | }, 40 | "diskUUID": { 41 | "properties": { 42 | "type": { "enum": [ "disk" ] }, 43 | "label": { 44 | "type": "string", 45 | "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" 46 | } 47 | }, 48 | "required": [ "type", "label" ], 49 | "additionalProperties": false 50 | }, 51 | "nfs": { 52 | "properties": { 53 | "type": { "enum": [ "nfs" ] }, 54 | "remotePath": { 55 | "type": "string", 56 | "pattern": "^(/[^/]+)+$" 57 | }, 58 | "server": { 59 | "type": "string", 60 | "oneOf": [ 61 | { "format": "host-name" }, 62 | { "format": "ipv4" }, 63 | { "format": "ipv6" } 64 | ] 65 | } 66 | }, 67 | "required": [ "type", "server", "remotePath" ], 68 | "additionalProperties": false 69 | }, 70 | "tmpfs": { 71 | "properties": { 72 | "type": { "enum": [ "tmpfs" ] }, 73 | "sizeInMB": { 74 | "type": "integer", 75 | "minimum": 16, 76 | "maximum": 512 77 | } 78 | }, 79 | "required": [ "type", "sizeInMB" ], 80 | "additionalProperties": false 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/fstab/example.js: -------------------------------------------------------------------------------- 1 | // 2 | // Example showing how to validate the "/etc/fstab" schema shown in 3 | // http://json-schema.org/example2.html. 4 | // 5 | 6 | var JaySchema = require('../../lib/jayschema.js') 7 | , assert = require('assert') 8 | , util = require('util') 9 | ; 10 | 11 | // Create the JaySchema object 12 | var js = new JaySchema(); 13 | 14 | // Grab some sample data to validate 15 | var data = require('./data.json'); 16 | 17 | // Get the main schema and the dependency "entry" schema 18 | var schema = require('./fstab-schema.json'); 19 | var entrySchema = require('./entry-schema.json'); 20 | 21 | // Register the dependency schema 22 | js.register(entrySchema); 23 | 24 | // Okay, let's validate. Synchronously this time. 25 | var errors = js.validate(data, schema); 26 | 27 | if (errors.length) { 28 | console.error('validation errors:\n', util.inspect(errors, false, null)); 29 | } 30 | else { 31 | console.log('no validation errors!'); 32 | } 33 | -------------------------------------------------------------------------------- /examples/fstab/fstab-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "/": { "$ref": "http://some.site.somewhere/entry-schema#" } 6 | }, 7 | "patternProperties": { 8 | "^(/[^/]+)+$": { "$ref": "http://some.site.somewhere/entry-schema#" } 9 | }, 10 | "additionalProperties": false, 11 | "required": [ "/" ] 12 | } 13 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | // 2 | // Error types for the JSON Schema validator. 3 | // 4 | 5 | 'use strict'; 6 | 7 | var util = require('util') 8 | ; 9 | 10 | 11 | // ****************************************************************** 12 | // Base error 13 | // ****************************************************************** 14 | 15 | var ValidationError = 16 | function(resolutionScope, instanceContext, constraintName, constraintValue, 17 | testedValue, desc) 18 | { 19 | Error.captureStackTrace(this, this.constructor); 20 | 21 | if (instanceContext !== undefined) { this.instanceContext = instanceContext; } 22 | else { this.instanceContext = '#/'; } 23 | 24 | if (resolutionScope !== undefined) { this.resolutionScope = resolutionScope; } 25 | if (constraintName !== undefined) { this.constraintName = constraintName; } 26 | if (constraintValue !== undefined) { this.constraintValue = constraintValue; } 27 | if (testedValue !== undefined) { this.testedValue = testedValue; } 28 | if (desc !== undefined) { this.desc = desc; } 29 | }; 30 | util.inherits(ValidationError, Error); 31 | 32 | // ****************************************************************** 33 | // More-specific error types 34 | // ****************************************************************** 35 | 36 | var NumericValidationError = function() { 37 | ValidationError.apply(this, arguments); 38 | this.kind = 'NumericValidationError'; 39 | }; 40 | util.inherits(NumericValidationError, ValidationError); 41 | 42 | var StringValidationError = function() { 43 | ValidationError.apply(this, arguments); 44 | this.kind = 'StringValidationError'; 45 | }; 46 | util.inherits(StringValidationError, ValidationError); 47 | 48 | var ArrayValidationError = function() { 49 | ValidationError.apply(this, arguments); 50 | this.kind = 'ArrayValidationError'; 51 | }; 52 | util.inherits(ArrayValidationError, ValidationError); 53 | 54 | var ObjectValidationError = function() { 55 | ValidationError.apply(this, arguments); 56 | this.kind = 'ObjectValidationError'; 57 | }; 58 | util.inherits(ObjectValidationError, ValidationError); 59 | 60 | var FormatValidationError = function() { 61 | ValidationError.apply(this, arguments); 62 | this.kind = 'FormatValidationError'; 63 | }; 64 | 65 | var LoaderAsyncError = function() { 66 | ValidationError.apply(this, arguments); 67 | this.kind = 'LoaderAsyncError'; 68 | }; 69 | util.inherits(ObjectValidationError, ValidationError); 70 | 71 | var SubSchemaValidationError = function() { 72 | ValidationError.apply(this, arguments); 73 | this.kind = 'SubSchemaValidationError'; 74 | if (arguments.length > 6) { 75 | this.subSchemaValidationErrors = arguments[6]; 76 | } 77 | }; 78 | util.inherits(SubSchemaValidationError, ValidationError); 79 | 80 | var SchemaLoaderError = function(ref, message, subError) { 81 | if (!message) { 82 | message = 'schema loader could not load schema for: ' + ref; 83 | } 84 | ValidationError.call(this, null, null, '$ref', ref, null, message); 85 | this.kind = 'SchemaLoaderError'; 86 | if (subError) { 87 | this.subError = subError; 88 | } 89 | }; 90 | util.inherits(SchemaLoaderError, ValidationError); 91 | 92 | 93 | // ****************************************************************** 94 | // Exports 95 | // ****************************************************************** 96 | 97 | exports.ValidationError = ValidationError; 98 | exports.NumericValidationError = NumericValidationError; 99 | exports.StringValidationError = StringValidationError; 100 | exports.ArrayValidationError = ArrayValidationError; 101 | exports.ObjectValidationError = ObjectValidationError; 102 | exports.FormatValidationError = FormatValidationError; 103 | exports.LoaderAsyncError = LoaderAsyncError; 104 | exports.SubSchemaValidationError = SubSchemaValidationError; 105 | exports.SchemaLoaderError = SchemaLoaderError; 106 | -------------------------------------------------------------------------------- /lib/httpLoader.js: -------------------------------------------------------------------------------- 1 | // 2 | // Example schema loader that loads schemas over HTTP[S]. 3 | // Wrapper for http.get. For SSL connections, the CA certificate is 4 | // not verified. 5 | // 6 | // If you were to write your own HTTP[S] loader, you’d probably want 7 | // to use https://github.com/mikeal/request to do the heavy lifting. 8 | // 9 | 10 | var http = require('http') 11 | , https = require('https') 12 | , Errors = require('./errors.js') 13 | , url = require('url') 14 | ; 15 | 16 | var MAX_DEPTH = 5; 17 | 18 | function loader(ref, callback, _depth) { 19 | 20 | _depth = _depth || 0; 21 | if (_depth >= MAX_DEPTH) { 22 | var desc = 'could not GET URL: ' + ref + ' (too many redirects)'; 23 | var err = new Errors.SchemaLoaderError(ref, desc); 24 | return callback(err); 25 | } 26 | 27 | var options = url.parse(ref); 28 | 29 | var getter = http.get; 30 | if (options.protocol === 'https:') { 31 | options.rejectUnauthorized = false; 32 | getter = https.get; 33 | } 34 | 35 | getter(options, function(res) { 36 | 37 | if (res.statusCode >= 300 && res.statusCode <= 399) { 38 | // redirect 39 | return process.nextTick(loader.bind(null, res.headers.location, 40 | callback, _depth + 1)); 41 | } 42 | 43 | if (res.statusCode < 200 || res.statusCode >= 400) { 44 | var desc = 'could not GET URL: ' + ref + ' (error ' + res.statusCode + 45 | ')'; 46 | var err = new Errors.SchemaLoaderError(ref, desc); 47 | return callback(err); 48 | } 49 | 50 | var json = ''; 51 | res.on('data', function(chunk) { 52 | json += chunk; 53 | }); 54 | 55 | res.on('end', function() { 56 | try { 57 | var schema = JSON.parse(json); 58 | return callback(null, schema); 59 | } catch (jsonError) { 60 | var desc = 'could not parse data from URL as JSON: ' + ref; 61 | var err = new Errors.SchemaLoaderError(ref, desc, jsonError); 62 | return callback(err); 63 | } 64 | }); 65 | 66 | res.on('error', function(httpError) { 67 | var desc = 'could not GET URL: ' + ref; 68 | var err = new Errors.SchemaLoaderError(ref, desc, httpError); 69 | callback(err); 70 | }); 71 | 72 | }); 73 | 74 | } 75 | 76 | module.exports = loader; 77 | -------------------------------------------------------------------------------- /lib/schemaRegistry.js: -------------------------------------------------------------------------------- 1 | // 2 | // Register schemas and retrieve previously-registered schemas. 3 | // 4 | 5 | var uri = require('./uri.js') 6 | , url = require('url') 7 | ; 8 | 9 | 10 | // Schemas for which we have our own bundled copy. These will be 11 | // registered automatically. 12 | var EMBEDDED_SCHEMAS = { 13 | 'http://json-schema.org/draft-04/schema#': 14 | require('./suites/draft-04/json-schema-draft-v4.json') 15 | }; 16 | 17 | 18 | // ****************************************************************** 19 | // Constructor 20 | // ****************************************************************** 21 | var SchemaRegistry = function() { 22 | this._schemas = {}; 23 | this._missingSchemas = {}; 24 | 25 | // register embedded schemas 26 | var keys = Object.keys(EMBEDDED_SCHEMAS); 27 | keys.forEach(function(key) { this.register(EMBEDDED_SCHEMAS[key]); }, this); 28 | }; 29 | 30 | 31 | // ****************************************************************** 32 | // [static] Decode an escaped JSON pointer 33 | // ****************************************************************** 34 | SchemaRegistry._decodeJsonPointer = function(pointer) { 35 | var result = pointer.replace(/\~1/g, '/'); 36 | return result.replace(/\~0/g, '~'); 37 | }; 38 | 39 | 40 | // ****************************************************************** 41 | // [static] Given a schema and a JSON pointer, return the 42 | // corresponding sub-schema referenced by the JSON pointer. 43 | // ****************************************************************** 44 | SchemaRegistry._resolveJsonPointer = function(schema, jp) { 45 | if (jp === '#') { 46 | return schema; 47 | } 48 | 49 | if (jp.slice(0, 2) !== '#/') { 50 | // not a JSON pointer fragment 51 | // (may be a valid id ref, but that’s not our problem here) 52 | return null; 53 | } 54 | 55 | var path = jp.slice(2).split('/'); 56 | var currentSchema = schema; 57 | while (path.length) { 58 | var element = SchemaRegistry._decodeJsonPointer(path.shift()); 59 | if (!Object.prototype.hasOwnProperty.call(currentSchema, element)) { 60 | return null; 61 | } 62 | currentSchema = currentSchema[element]; 63 | } 64 | 65 | return currentSchema; 66 | }; 67 | 68 | 69 | // ****************************************************************** 70 | // [static] Normalize schema ids so they can be looked up. 71 | // ****************************************************************** 72 | SchemaRegistry._normalizeId = function(id) { 73 | // for internal use we add the '#' (empty fragment) if missing 74 | if (id.indexOf('#') === -1) { return id + '#'; } 75 | var uriObj = uri.parse(id); 76 | if (uriObj.kind === 'url') { id = url.resolve(id, id); } 77 | return id; 78 | }; 79 | 80 | 81 | // ****************************************************************** 82 | // Return boolean indicating whether the specified schema id has 83 | // previously been registered. 84 | // ****************************************************************** 85 | SchemaRegistry.prototype.isRegistered = function(id) { 86 | if (!id) { return false; } 87 | id = SchemaRegistry._normalizeId(id); 88 | if (this._schemas.hasOwnProperty(id)) { return true; } 89 | var uriObj = uri.parse(id); 90 | return this._schemas.hasOwnProperty(uriObj.baseUri); 91 | }; 92 | 93 | 94 | // ****************************************************************** 95 | // [static] Helper to descend into an object, recursively gathering 96 | // all $refs values from the given object and its sub-objects. 97 | // ****************************************************************** 98 | SchemaRegistry._gatherRefs = function(obj) { 99 | var result = []; 100 | 101 | var currentObj = obj; 102 | var subObjects = []; 103 | 104 | do { 105 | 106 | if (Object.prototype.hasOwnProperty.call(currentObj, '$ref')) { 107 | result.push(currentObj.$ref); 108 | } 109 | 110 | var keys = Object.keys(currentObj); 111 | for (var index = 0, len = keys.length; index !== len; ++index) { 112 | var prop = currentObj[keys[index]]; 113 | if (typeof prop === 'object') { subObjects.push(prop); } 114 | } 115 | 116 | currentObj = subObjects.pop(); 117 | 118 | } while(currentObj); 119 | 120 | return result; 121 | }; 122 | 123 | 124 | // ****************************************************************** 125 | // [static] Helper to descend into an object, recursively gathering 126 | // all sub-objects that contain an id property. 127 | // 128 | // Returns an array where each element is an array of the form: 129 | // 130 | // [ schema, resolutionScope ] 131 | // ****************************************************************** 132 | SchemaRegistry._getSubObjectsHavingIds = function(obj, resolutionScope) { 133 | var result = []; 134 | 135 | resolutionScope = resolutionScope || '#'; 136 | var currentObj = obj; 137 | var subObjects = []; 138 | var nextItem; 139 | 140 | do { 141 | 142 | if (currentObj !== null && currentObj !== undefined) { 143 | if (Object.prototype.hasOwnProperty.call(currentObj, 'id') && 144 | typeof currentObj.id === 'string') 145 | { 146 | result.push([currentObj, resolutionScope]); 147 | resolutionScope = uri.resolve(resolutionScope, currentObj.id); 148 | } 149 | 150 | var keys = Object.keys(currentObj); 151 | for (var index = 0, len = keys.length; index !== len; ++index) { 152 | var prop = currentObj[keys[index]]; 153 | if (typeof prop === 'object') { 154 | subObjects.push([prop, resolutionScope + '/' + keys[index]]); 155 | } 156 | } 157 | } 158 | 159 | nextItem = subObjects.pop(); 160 | if (nextItem) { 161 | currentObj = nextItem[0]; 162 | resolutionScope = nextItem[1]; 163 | } 164 | } while(nextItem); 165 | 166 | return result; 167 | }; 168 | 169 | 170 | // ****************************************************************** 171 | // Return currently-unregistered schemas $referenced by the given 172 | // schema. 173 | // ****************************************************************** 174 | SchemaRegistry.prototype._missingRefsForSchema = function(schema) { 175 | var allRefs = SchemaRegistry._gatherRefs(schema); 176 | var missingRefs = []; 177 | 178 | allRefs.forEach(function(ref) { 179 | if (ref[0] !== '#') { 180 | var uriObj = uri.parse(ref); 181 | if (!this.isRegistered(uriObj.baseUri)) { 182 | missingRefs.push(uriObj.baseUri); 183 | } 184 | } 185 | }, this); 186 | 187 | return missingRefs; 188 | }; 189 | 190 | 191 | // ****************************************************************** 192 | // Register a schema (internal implementation) 193 | // ****************************************************************** 194 | SchemaRegistry.prototype._registerImpl = function(schema, id, _resolutionScope) 195 | { 196 | // sanity check 197 | if (typeof schema !== 'object' || Array.isArray(schema)) { return []; } 198 | 199 | if (id) { 200 | var resolvedId = uri.resolve(_resolutionScope || id, id); 201 | if (this.isRegistered(resolvedId)) { return; } 202 | 203 | var uriObj = uri.parse(resolvedId); 204 | if (!uriObj.baseUri) { return; } 205 | 206 | this._schemas[resolvedId] = schema; 207 | } 208 | }; 209 | 210 | // ****************************************************************** 211 | // Register a schema (public interface) 212 | // ****************************************************************** 213 | SchemaRegistry.prototype.register = function(schema, id) { 214 | id = id || schema.id; 215 | this._registerImpl(schema, id); 216 | 217 | // register any id'd sub-objects 218 | var toRegister = SchemaRegistry._getSubObjectsHavingIds(schema, id); 219 | toRegister.forEach(function(item) { 220 | this._registerImpl(item[0], item[0].id, item[1]); 221 | }, this); 222 | 223 | // save any missing refs to support the getMissingSchemas method 224 | var missing = this._missingRefsForSchema(schema); 225 | missing.forEach(function(item) { 226 | this._missingSchemas[item] = true; 227 | }, this); 228 | 229 | return missing; 230 | }; 231 | 232 | // ****************************************************************** 233 | // Get an array of $refs for which we don’t have a schema yet. 234 | // ****************************************************************** 235 | SchemaRegistry.prototype.getMissingSchemas = function() { 236 | var result = Object.keys(this._missingSchemas); 237 | result = result.filter(function(item) { 238 | return !this.isRegistered(item); 239 | }, this); 240 | return result; 241 | }; 242 | 243 | // ****************************************************************** 244 | // Retrieve a previously-registered schema. 245 | // ****************************************************************** 246 | SchemaRegistry.prototype.get = function(id) { 247 | 248 | id = SchemaRegistry._normalizeId(id); 249 | 250 | if (this.isRegistered(id)) { 251 | return this._schemas[id]; 252 | } 253 | 254 | var uriObj = uri.parse(id); 255 | 256 | if (uriObj.fragment.slice(0, 2) === '#/') { 257 | return SchemaRegistry._resolveJsonPointer( 258 | this._schemas[uriObj.baseUri + '#'], 259 | uriObj.fragment 260 | ); 261 | } 262 | }; 263 | 264 | module.exports = SchemaRegistry; 265 | -------------------------------------------------------------------------------- /lib/suites/draft-04/core.js: -------------------------------------------------------------------------------- 1 | 2 | // ****************************************************************** 3 | // Equality as defined in the JSON Schema spec. 4 | // ****************************************************************** 5 | function jsonEqual(x, y) { 6 | var index, len; 7 | 8 | if (Array.isArray(x)) { 9 | if (!Array.isArray(y)) { return false; } 10 | if (x.length !== y.length) { return false; } 11 | for (index = 0, len = x.length; index !== len; ++index) { 12 | if (!jsonEqual(x[index], y[index])) { return false; } 13 | } 14 | return true; 15 | } 16 | 17 | if (typeof x === 'object' && x !== null) { 18 | if (typeof y !== 'object' || y === null) { return false; } 19 | var xkeys = Object.keys(x); 20 | if (xkeys.length !== Object.keys(y).length) { return false; } 21 | for (index = 0, len = xkeys.length; index !== len; ++index) { 22 | var key = xkeys[index]; 23 | if (!Object.prototype.hasOwnProperty.call(y, key) || 24 | !jsonEqual(x[key], y[key])) 25 | { 26 | return false; 27 | } 28 | } 29 | return true; 30 | } 31 | 32 | return x === y; // scalar value (boolean, string, number) 33 | } 34 | exports.jsonEqual = jsonEqual; 35 | 36 | // ****************************************************************** 37 | // Given an instance value, get its apparent primitive type. 38 | // ****************************************************************** 39 | function apparentType(val) { 40 | switch (typeof val) { 41 | case 'boolean': 42 | case 'string': 43 | case 'undefined': 44 | return typeof val; 45 | 46 | case 'number': 47 | if (val % 1 === 0) { return 'integer'; } 48 | return 'number'; 49 | 50 | default: 51 | if (val === null) { return 'null'; } 52 | if (Array.isArray(val)) { return 'array'; } 53 | return 'object'; 54 | } 55 | } 56 | exports.apparentType = apparentType; 57 | 58 | // ****************************************************************** 59 | // Helper function to get the value of a schema property. 60 | // ****************************************************************** 61 | function getSchemaProperty(schema, propName, defaultValue) { 62 | if (Object.prototype.hasOwnProperty.call(schema, propName)) { 63 | return schema[propName]; 64 | } else { 65 | return defaultValue; 66 | } 67 | } 68 | exports.getSchemaProperty = getSchemaProperty; 69 | 70 | // ****************************************************************** 71 | // RegExps for use with the format keyword. 72 | // ****************************************************************** 73 | exports.FORMAT_REGEXPS = { 74 | 75 | 'date-time': new RegExp( 76 | '^' + 77 | '(\\d{4})\\-(\\d{2})\\-(\\d{2})' + // full-date 78 | '[T ]' + 79 | '(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?' + // partial-time 80 | '(Z|(?:([\\+|\\-])(\\d{2}):(\\d{2})))' + // time-offset 81 | '$' 82 | ), 83 | 84 | date: new RegExp( 85 | '^' + 86 | '(\\d{4})\\-(\\d{2})\\-(\\d{2})' + 87 | '$' 88 | ), 89 | 90 | time: new RegExp( 91 | '^' + 92 | '(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?' + 93 | '$' 94 | ), 95 | 96 | hostname: new RegExp( 97 | '^' + 98 | '[A-Za-z0-9]' + // must start with a letter or digit 99 | '(?:' + 100 | '[A-Za-z0-9-]*' + // optional letters/digits/hypens 101 | '[A-Za-z0-9]' + // must not end with a hyphen 102 | ')?' + 103 | '(?:' + 104 | '\\.' + 105 | '[A-Za-z0-9]' + // must start with a letter or digit 106 | '(?:' + 107 | '[A-Za-z0-9-]*' + // optional letters/digits/hypens 108 | '[A-Za-z0-9]' + // must not end with a hyphen 109 | ')?' + 110 | ')*' + 111 | '$' 112 | ), 113 | 114 | email: new RegExp( 115 | '^' + 116 | '[A-Za-z0-9!#$%&\'*+=/?^_`{|}~-]+' + 117 | '(?:\\.[A-Za-z0-9!#$%&\'*+=/?^_`{|}~-]+)*' + // dot-atom 118 | '@' + 119 | '(' + 120 | '[A-Za-z0-9]' + // must start with a letter or digit 121 | '(?:' + 122 | '[A-Za-z0-9-]*' + // optional letters/digits/hypens 123 | '[A-Za-z0-9]' + // must not end with a hyphen 124 | ')?' + 125 | '(?:' + 126 | '\\.' + 127 | '[A-Za-z0-9]' + // must start with a letter or digit 128 | '(?:' + 129 | '[A-Za-z0-9-]*' + // optional letters/digits/hypens 130 | '[A-Za-z0-9]' + // must not end with a hyphen 131 | ')?' + 132 | ')*' + 133 | ')' + 134 | '$' 135 | ), 136 | 137 | ipv4: new RegExp( 138 | '^' + 139 | '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})' + 140 | '(?:' + 141 | '\\.' + 142 | '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})' + 143 | '){3}' + 144 | '$' 145 | ), 146 | 147 | ipv6: { 148 | allForms: new RegExp( 149 | '^' + 150 | '[0-9A-Fa-f\\:\\.]{2,45}' + 151 | '$' 152 | ), 153 | 154 | form1: new RegExp( 155 | '^' + 156 | '[0-9A-Fa-f]{1,4}' + 157 | '(?:' + 158 | ':' + 159 | '[0-9A-Fa-f]{1,4}' + 160 | '){7}' + 161 | '$' 162 | ), 163 | 164 | form3full: new RegExp( 165 | '^' + 166 | '(' + 167 | '[0-9A-Fa-f]{1,4}:' + 168 | '){6}' + 169 | '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})' + 170 | '(?:' + 171 | '\\.' + 172 | '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})' + 173 | '){3}' + 174 | '$' 175 | ) 176 | }, 177 | 178 | uri: new RegExp( 179 | '^' + 180 | '([A-Za-z][A-Za-z0-9+.-]*:/{0,3})?' + // scheme 181 | '[A-Za-z0-9\\[\\]._~%!$&\'()*+,;=:@/-]*' + // hier-part 182 | '(' + 183 | '[?][A-Za-z0-9._~%!$&\'()*+,;=:@/?-]*' + // query 184 | ')?' + 185 | '(' + 186 | '#[A-Za-z0-9._~%!$&\'()*+,;=:@/?-]*' + // fragment 187 | ')?' + 188 | '$' 189 | ) 190 | }; 191 | -------------------------------------------------------------------------------- /lib/suites/draft-04/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Validation suite for JSON objects against a JSON Schema Draft v4 3 | // schema. Each test returns an array (possibly empty) of errors. 4 | // 5 | 6 | 'use strict'; 7 | 8 | var core = require('./core.js') 9 | , Errors = require('../../errors.js') 10 | , uri = require('../../uri.js') 11 | ; 12 | 13 | // ****************************************************************** 14 | // Categories of properties we can validate. 15 | // ****************************************************************** 16 | var PROPS_TO_VALIDATE = { 17 | general: ['enum', 'allOf', 'anyOf', 'oneOf', 'not'], 18 | array: ['items', 'additionalItems', 'maxItems', 'minItems', 'uniqueItems'], 19 | number: ['multipleOf', 'maximum', 'exclusiveMaximum', 'minimum', 20 | 'exclusiveMinimum'], 21 | object: ['maxProperties', 'minProperties', 'required', 22 | 'additionalProperties', 'properties', 'patternProperties', 23 | 'dependencies'], 24 | string: ['maxLength', 'minLength', 'pattern', 'format'] 25 | }; 26 | 27 | var VALIDATOR_FUNCTIONS = { 28 | type: require('./keywords/type.js') 29 | }; 30 | 31 | // ****************************************************************** 32 | // Return a set of tests to apply to the instance. 33 | // ****************************************************************** 34 | function getApplicableTests(config) { 35 | var result = []; 36 | var len, i, key; 37 | 38 | // general tests that apply to all types 39 | for (i = 0, len = PROPS_TO_VALIDATE.general.length; i !== len; ++i) { 40 | key = PROPS_TO_VALIDATE.general[i]; 41 | if (Object.prototype.hasOwnProperty.call(config.schema, key)) { 42 | result.push(key); 43 | } 44 | } 45 | 46 | // type-specific tests 47 | var apparentType = core.apparentType(config.inst); 48 | if (apparentType === 'integer') { apparentType = 'number'; } 49 | var props = PROPS_TO_VALIDATE[apparentType] || []; 50 | for (i = 0, len = props.length; i !== len; ++i) 51 | { 52 | key = props[i]; 53 | if (Object.prototype.hasOwnProperty.call(config.schema, key)) { 54 | result.push(key); 55 | } 56 | } 57 | 58 | // for objects: the properties, patternProperties, and 59 | // additionalProperties validations are inseparable 60 | if (result.indexOf('properties') !== -1 || 61 | result.indexOf('patternProperties') !== -1 || 62 | result.indexOf('additionalProperties') !== -1) 63 | { 64 | result.push('_propertiesImpl'); 65 | } 66 | 67 | return result; 68 | } 69 | 70 | // ****************************************************************** 71 | // Run all applicable tests. 72 | // 73 | // config values: 74 | // 75 | // inst: instance to validate 76 | // schema: schema to validate against 77 | // resolutionScope: resolutionScope, 78 | // instanceContext: current position within the overall instance 79 | // schemaRegistry: a SchemaRegistry 80 | // customFormatHandlers: custom handlers for the "format" keyword 81 | // 82 | // ****************************************************************** 83 | function run(config) 84 | { 85 | var errors = []; 86 | var desc; 87 | 88 | var maxRefDepth = 10; // avoid infinite loops 89 | var depth = 0; 90 | 91 | while (depth < maxRefDepth && 92 | config.schema && 93 | Object.prototype.hasOwnProperty.call(config.schema, '$ref')) 94 | { 95 | var ref = uri.resolve(config.resolutionScope, 96 | decodeURI(config.schema.$ref)); 97 | config.schema = config.schemaRegistry.get(ref); 98 | 99 | if (!config.schema) { 100 | desc = 'schema not available: ' + ref; 101 | errors.push(new Errors.ValidationError(undefined, undefined, undefined, 102 | undefined, undefined, desc)); 103 | return errors; 104 | } 105 | 106 | config.resolutionScope = ref; 107 | depth++; 108 | 109 | if (depth >= maxRefDepth) { 110 | desc = 'maximum nested $ref depth of ' + maxRefDepth + ' exceeded; ' + 111 | 'possible $ref loop'; 112 | errors.push(new Errors.ValidationError(undefined, config.instanceContext, 113 | '$ref', undefined, undefined, desc)); 114 | return errors; 115 | } 116 | } 117 | 118 | // empty schema - bail early 119 | if (Object.keys(config.schema).length === 0) { return errors; } 120 | 121 | // validate the type: if it isn't valid we can bail early 122 | errors = errors.concat(VALIDATOR_FUNCTIONS.type(config)); 123 | if (errors.length) { return errors; } 124 | 125 | // test all applicable schema properties 126 | var props = getApplicableTests(config); 127 | for (var index = 0; index < props.length; ++index) { 128 | var prop = props[index]; 129 | var fn = VALIDATOR_FUNCTIONS[prop]; 130 | if (!fn) { 131 | fn = VALIDATOR_FUNCTIONS[prop] = require('./keywords/' + prop + '.js'); 132 | } 133 | errors = errors.concat(fn(config)); 134 | } 135 | return errors; 136 | } 137 | 138 | module.exports = run; 139 | -------------------------------------------------------------------------------- /lib/suites/draft-04/json-schema-draft-v4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "$schema": { 35 | "type": "string", 36 | "format": "uri" 37 | }, 38 | "title": { 39 | "type": "string" 40 | }, 41 | "description": { 42 | "type": "string" 43 | }, 44 | "default": {}, 45 | "multipleOf": { 46 | "type": "number", 47 | "minimum": 0, 48 | "exclusiveMinimum": true 49 | }, 50 | "maximum": { 51 | "type": "number" 52 | }, 53 | "exclusiveMaximum": { 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "minimum": { 58 | "type": "number" 59 | }, 60 | "exclusiveMinimum": { 61 | "type": "boolean", 62 | "default": false 63 | }, 64 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 65 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 66 | "pattern": { 67 | "type": "string", 68 | "format": "regex" 69 | }, 70 | "additionalItems": { 71 | "anyOf": [ 72 | { "type": "boolean" }, 73 | { "$ref": "#" } 74 | ], 75 | "default": {} 76 | }, 77 | "items": { 78 | "anyOf": [ 79 | { "$ref": "#" }, 80 | { "$ref": "#/definitions/schemaArray" } 81 | ], 82 | "default": {} 83 | }, 84 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 85 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 86 | "uniqueItems": { 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 91 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "required": { "$ref": "#/definitions/stringArray" }, 93 | "additionalProperties": { 94 | "anyOf": [ 95 | { "type": "boolean" }, 96 | { "$ref": "#" } 97 | ], 98 | "default": {} 99 | }, 100 | "definitions": { 101 | "type": "object", 102 | "additionalProperties": { "$ref": "#" }, 103 | "default": {} 104 | }, 105 | "properties": { 106 | "type": "object", 107 | "additionalProperties": { "$ref": "#" }, 108 | "default": {} 109 | }, 110 | "patternProperties": { 111 | "type": "object", 112 | "additionalProperties": { "$ref": "#" }, 113 | "default": {} 114 | }, 115 | "dependencies": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "anyOf": [ 119 | { "$ref": "#" }, 120 | { "$ref": "#/definitions/stringArray" } 121 | ] 122 | } 123 | }, 124 | "enum": { 125 | "type": "array", 126 | "minItems": 1, 127 | "uniqueItems": true 128 | }, 129 | "type": { 130 | "anyOf": [ 131 | { "$ref": "#/definitions/simpleTypes" }, 132 | { 133 | "type": "array", 134 | "items": { "$ref": "#/definitions/simpleTypes" }, 135 | "minItems": 1, 136 | "uniqueItems": true 137 | } 138 | ] 139 | }, 140 | "allOf": { "$ref": "#/definitions/schemaArray" }, 141 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 142 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 143 | "not": { "$ref": "#" } 144 | }, 145 | "dependencies": { 146 | "exclusiveMaximum": [ "maximum" ], 147 | "exclusiveMinimum": [ "minimum" ] 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/_propertiesImpl.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.4. Validation keywords for objects 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js') 6 | , core = require('../core.js') 7 | , testRunner = require('../index.js') 8 | ; 9 | 10 | module.exports = function(config) { 11 | var errors = []; 12 | var keys, key, index, subTestConfig; 13 | 14 | var p = core.getSchemaProperty(config.schema, 'properties', {}); 15 | var additionalProperties = core.getSchemaProperty(config.schema, 16 | 'additionalProperties', {}); 17 | 18 | // for patternProperties, compile RegExps just once 19 | var pp = []; 20 | if (Object.prototype.hasOwnProperty.call(config.schema, 'patternProperties')) 21 | { 22 | keys = Object.keys(config.schema.patternProperties); 23 | for (index = 0; index < keys.length; ++index) { 24 | key = keys[index]; 25 | pp.push([new RegExp(key), config.schema.patternProperties[key]]); 26 | } 27 | } 28 | 29 | // for each property, validate against matching property schemas 30 | keys = Object.keys(config.inst); 31 | for (var x = 0; x < keys.length; ++x) { 32 | var m = keys[x]; 33 | 34 | var context = config.instanceContext + '/' + m; 35 | var applyAdditionalProperties = true; 36 | 37 | if (Object.prototype.hasOwnProperty.call(p, m)) { 38 | subTestConfig = config.clone(); 39 | subTestConfig.inst = config.inst[m]; 40 | subTestConfig.schema = p[m]; 41 | subTestConfig.resolutionScope = 42 | config.resolutionScope + '/properties/' + m; 43 | subTestConfig.instanceContext = context; 44 | errors = errors.concat(testRunner(subTestConfig)); 45 | applyAdditionalProperties = false; 46 | } 47 | 48 | for (var y = 0; y < pp.length; ++y) { 49 | var rx = pp[y][0]; 50 | if (m.match(rx)) { 51 | subTestConfig = config.clone(); 52 | subTestConfig.inst = config.inst[m]; 53 | subTestConfig.schema = pp[y][1]; 54 | subTestConfig.resolutionScope = 55 | config.resolutionScope + '/patternProperties/' + m; 56 | subTestConfig.instanceContext = context; 57 | errors = errors.concat(testRunner(subTestConfig)); 58 | applyAdditionalProperties = false; 59 | } 60 | } 61 | 62 | if (applyAdditionalProperties) { 63 | if (additionalProperties === false) { 64 | var desc = 'property "' + m + '" not allowed by "properties" or by ' + 65 | '"patternProperties" and "additionalProperties" is false'; 66 | errors.push(new Errors.ObjectValidationError(config.resolutionScope, 67 | config.instanceContext, 'additionalProperties', undefined, m, desc)); 68 | } else if (additionalProperties !== true) { 69 | subTestConfig = config.clone(); 70 | subTestConfig.inst = config.inst[m]; 71 | subTestConfig.schema = additionalProperties; 72 | subTestConfig.resolutionScope = 73 | config.resolutionScope + '/additionalProperties'; 74 | subTestConfig.instanceContext = context; 75 | errors = errors.concat(testRunner(subTestConfig)); 76 | } 77 | } 78 | } 79 | 80 | return errors; 81 | }; 82 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/additionalItems.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.3. Validation keywords for arrays 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) 8 | { 9 | var errors = []; 10 | 11 | // always succeeds in these conditions 12 | if (config.schema.additionalItems === true || 13 | typeof config.schema.additionalItems === 'object' || 14 | !Object.prototype.hasOwnProperty.call(config.schema, 'items') || 15 | typeof config.schema.items === 'object' && 16 | !Array.isArray(config.schema.items)) 17 | { 18 | return errors; 19 | } 20 | 21 | // config.schema.items must be an Array if we’ve reached this point 22 | 23 | if (config.schema.additionalItems === false && 24 | config.inst.length > config.schema.items.length) 25 | { 26 | var desc = 'array length (' + config.inst.length + ') is greater than ' + 27 | '"items" length (' + config.schema.items.length + ') and ' + 28 | '"additionalItems" is false'; 29 | errors.push(new Errors.ArrayValidationError(config.resolutionScope, 30 | config.instanceContext, 'additionalItems', undefined, undefined, desc)); 31 | } 32 | 33 | return errors; 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/additionalProperties.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.4. Validation keywords for objects 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | // no-op (_propertiesImpl will validate this) 9 | return []; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/allOf.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.5. Validation keywords for any instance type 3 | // ****************************************************************** 4 | 5 | var testRunner = require('../index.js') 6 | ; 7 | 8 | module.exports = function(config) { 9 | var errors = []; 10 | for (var index = 0; index < config.schema.allOf.length; ++index) { 11 | var schema = config.schema.allOf[index]; 12 | 13 | var subTestConfig = config.clone(); 14 | subTestConfig.schema = schema; 15 | subTestConfig.resolutionScope = config.resolutionScope + '/allOf/' + index; 16 | 17 | errors = errors.concat(testRunner(subTestConfig)); 18 | } 19 | return errors; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/anyOf.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.5. Validation keywords for any instance type 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js') 6 | , testRunner = require('../index.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | 11 | var errors = []; 12 | var subSchemaErrors = {}; 13 | 14 | for (var index = 0, len = config.schema.anyOf.length; index !== len; ++index) 15 | { 16 | var subTestConfig = config.clone(); 17 | subTestConfig.schema = config.schema.anyOf[index]; 18 | subTestConfig.resolutionScope = config.resolutionScope + '/anyOf/' + index; 19 | 20 | var nestedErrors = testRunner(subTestConfig); 21 | 22 | if (nestedErrors.length === 0) { 23 | return errors; 24 | } else { 25 | var key = undefined; 26 | if (Object.prototype.hasOwnProperty.call(config.schema.anyOf[index], 27 | '$ref')) 28 | { 29 | key = config.schema.anyOf[index].$ref; 30 | } 31 | if (!key) { 32 | key = subTestConfig.schema.id || 'sub-schema-' + (index + 1); 33 | } 34 | subSchemaErrors[key] = nestedErrors; 35 | } 36 | } 37 | 38 | errors.push(new Errors.SubSchemaValidationError(config.resolutionScope, 39 | config.instanceContext, 'anyOf', config.schema.anyOf, undefined, 40 | 'does not validate against any of these schemas; it must validate ' + 41 | 'against at least one', subSchemaErrors)); 42 | 43 | return errors; 44 | }; 45 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/dependencies.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.4. Validation keywords for objects 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js') 6 | , testRunner = require('../index.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | var i, prop, len; 12 | 13 | var deps = Object.keys(config.schema.dependencies); 14 | 15 | var depsToApply = []; 16 | for (i = 0, len = deps.length; i !== len; ++i) { 17 | prop = deps[i]; 18 | if (Object.prototype.hasOwnProperty.call(config.inst, prop)) { 19 | depsToApply.push(prop); 20 | } 21 | } 22 | 23 | for (var index = 0; index < depsToApply.length; ++index) { 24 | var key = depsToApply[index]; 25 | var dep = config.schema.dependencies[key]; 26 | 27 | if (Array.isArray(dep)) { 28 | // property dependency 29 | var missing = []; 30 | for (i = 0, len = dep.length; i !== len; ++i) { 31 | prop = dep[i]; 32 | if (!Object.prototype.hasOwnProperty.call(config.inst, prop)) { 33 | missing.push(prop); 34 | } 35 | } 36 | 37 | if (missing.length) { 38 | errors.push(new Errors.ObjectValidationError(config.resolutionScope, 39 | config.instanceContext, 'dependencies', {key: dep}, undefined, 40 | 'missing: ' + missing)); 41 | } 42 | } else { 43 | // schema dependency: validates the *instance*, not the value 44 | // associated with the property name. 45 | var subTestConfig = config.clone(); 46 | subTestConfig.schema = dep; 47 | subTestConfig.resolutionScope = 48 | config.resolutionScope + '/dependencies/' + index; 49 | subTestConfig.instanceContext = config.instanceContext + '/' + key; 50 | errors = errors.concat(testRunner(subTestConfig)); 51 | } 52 | } 53 | 54 | return errors; 55 | }; 56 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/enum.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.5. Validation keywords for any instance type 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js') 6 | , core = require('../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | for (var index = 0, len = config.schema['enum'].length; index !== len; 12 | ++index) 13 | { 14 | if (core.jsonEqual(config.inst, config.schema['enum'][index])) { 15 | return errors; 16 | } 17 | } 18 | 19 | errors.push(new Errors.ValidationError(config.resolutionScope, 20 | config.instanceContext, 'enum', config.schema['enum'], config.inst)); 21 | 22 | return errors; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/exclusiveMaximum.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.1. Validation keywords for numeric instances 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | if (config.schema.exclusiveMaximum) { 10 | if (config.inst >= config.schema.maximum) { 11 | errors.push(new Errors.NumericValidationError(config.resolutionScope, 12 | config.instanceContext, 'exclusiveMaximum', config.schema.maximum, 13 | config.inst)); 14 | } 15 | } 16 | return errors; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/exclusiveMinimum.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.1. Validation keywords for numeric instances 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | if (config.schema.exclusiveMinimum) { 10 | if (config.inst <= config.schema.minimum) { 11 | errors.push(new Errors.NumericValidationError(config.resolutionScope, 12 | config.instanceContext, 'exclusiveMinimum', config.schema.minimum, 13 | config.inst)); 14 | } 15 | } 16 | return errors; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/format.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.5. Validation keywords for any instance type 3 | // ****************************************************************** 4 | 5 | var SUPPORTED_FORMATS = { 6 | 'date-time': require('./formats/date-time.js'), 7 | 'date': require('./formats/date.js'), 8 | 'time': require('./formats/time.js'), 9 | 'email': require('./formats/email.js'), 10 | 'hostname': require('./formats/hostname.js'), 11 | 'ipv4': require('./formats/ipv4.js'), 12 | 'ipv6': require('./formats/ipv6.js'), 13 | 'uri': require('./formats/uri.js') 14 | }; 15 | 16 | module.exports = function(config) { 17 | var errors = []; 18 | 19 | if (config.customFormatHandlers.hasOwnProperty(config.schema.format)) { 20 | errors = 21 | errors.concat(config.customFormatHandlers[config.schema.format](config)); 22 | } else if (SUPPORTED_FORMATS.hasOwnProperty(config.schema.format)) { 23 | errors = errors.concat(SUPPORTED_FORMATS[config.schema.format](config)); 24 | } 25 | 26 | return errors; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/formats/date-time.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // Format keyword 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../../errors.js') 6 | , core = require('../../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | var valid = false; 12 | 13 | var match = config.inst.match(core.FORMAT_REGEXPS['date-time']); 14 | 15 | if (match) { 16 | var year = parseInt(match[1], 10); 17 | var month = parseInt(match[2], 10); 18 | var mday = parseInt(match[3], 10); 19 | var hour = parseInt(match[4], 10); 20 | var min = parseInt(match[5], 10); 21 | var sec = parseInt(match[6], 10); 22 | 23 | if ( 24 | month >= 1 && month <= 12 && 25 | mday >= 1 && mday <= 31 && 26 | hour >= 0 && hour <= 23 && 27 | min >= 0 && min <= 59 && 28 | sec >= 0 && sec <= 60 // it’s 60 during a leap second 29 | ) 30 | { 31 | var d = new Date(year, (month - 1) + 1); // the next month 32 | var lastDay = new Date(d - 86400000); 33 | if (mday <= lastDay.getDate()) { 34 | 35 | // [day-of-month is valid] 36 | 37 | if (match[10]) { 38 | var offsetHour = parseInt(match[10], 10); 39 | var offsetMin = parseInt(match[11], 10); 40 | if ( 41 | offsetHour >=0 && offsetHour <= 23 && 42 | offsetMin >= 0 && offsetMin <= 59 43 | ) 44 | { 45 | valid = true; 46 | } 47 | } else { 48 | valid = true; 49 | } 50 | } 51 | } 52 | } 53 | 54 | if (!valid) { 55 | var desc = 'not a valid date-time per RFC 3339 section 5.6 ' + 56 | '(use "date" for date-only or "time" for time-only)'; 57 | errors.push(new Errors.FormatValidationError(config.resolutionScope, 58 | config.instanceContext, 'format', 'date-time', config.inst, desc)); 59 | } 60 | 61 | return errors; 62 | }; 63 | 64 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/formats/date.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // Format keyword 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../../errors.js') 6 | , core = require('../../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | var valid = false; 12 | 13 | var match = config.inst.match(core.FORMAT_REGEXPS.date); 14 | 15 | if (match) { 16 | var year = parseInt(match[1], 10); 17 | var month = parseInt(match[2], 10); 18 | var mday = parseInt(match[3], 10); 19 | 20 | if ( 21 | month >= 1 && month <= 12 && 22 | mday >= 1 && mday <= 31 23 | ) 24 | { 25 | var d = new Date(year, (month - 1) + 1); // the next month 26 | var lastDay = new Date(d - 86400000); 27 | if (mday <= lastDay.getDate()) { 28 | // [day-of-month is valid] 29 | valid = true; 30 | } 31 | } 32 | } 33 | 34 | if (!valid) { 35 | var desc = 'not a valid date'; 36 | errors.push(new Errors.FormatValidationError(config.resolutionScope, 37 | config.instanceContext, 'format', 'date', config.inst, desc)); 38 | } 39 | 40 | return errors; 41 | }; 42 | 43 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/formats/email.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // Format keyword 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../../errors.js') 6 | , core = require('../../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | var valid = config.inst.match(core.FORMAT_REGEXPS.email); 12 | if (!valid) { 13 | var desc = 'not a valid email address'; 14 | errors.push(new Errors.FormatValidationError(config.resolutionScope, 15 | config.instanceContext, 'format', 'email', config.inst, desc)); 16 | } 17 | return errors; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/formats/hostname.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // Format keyword 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../../errors.js') 6 | , core = require('../../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | 12 | var valid = config.inst.match(core.FORMAT_REGEXPS.hostname); 13 | if (valid) { 14 | // per RFC 1035 “Preferred name syntax” each label must be no 15 | // more than 63 characters. 16 | var labels = config.inst.split('.'); 17 | for (var index = 0, len = labels.length; valid && index !== len; ++index) { 18 | if (labels[index].length > 63) { valid = false; } 19 | } 20 | 21 | // the final label must not start with a digit 22 | if (labels[labels.length - 1].match(/^[0-9]/)) { 23 | valid = false; 24 | } 25 | } 26 | 27 | if (!valid) { 28 | var desc = 'not a valid hostname per RFC 1034 Preferred Name Syntax'; 29 | errors.push(new Errors.FormatValidationError(config.resolutionScope, 30 | config.instanceContext, 'format', 'hostname', config.inst, desc)); 31 | } 32 | 33 | return errors; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/formats/ipv4.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // Format keyword 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../../errors.js') 6 | , core = require('../../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | 12 | if (!config.inst.match(core.FORMAT_REGEXPS.ipv4)) { 13 | var desc = 'not a valid dotted-quad IPv4 address'; 14 | errors.push(new Errors.FormatValidationError(config.resolutionScope, 15 | config.instanceContext, 'format', 'ipv4', config.inst, desc)); 16 | } 17 | 18 | return errors; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/formats/ipv6.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // Format keyword 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../../errors.js') 6 | , core = require('../../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | var index, len; 12 | 13 | // *** form 1 14 | if (config.inst.match(core.FORMAT_REGEXPS.ipv6.form1)) { return errors; } 15 | if (config.inst.match(core.FORMAT_REGEXPS.ipv6.form3full)) { return errors; } 16 | 17 | if (config.inst.match(core.FORMAT_REGEXPS.ipv6.allForms)) { 18 | var parts = config.inst.split(':'); 19 | if ( 20 | parts.length >= 3 && 21 | parts.length <= 8 && 22 | ( 23 | config.inst.indexOf('.') === -1 || 24 | config.inst.indexOf('.') > config.inst.lastIndexOf(':') 25 | ) 26 | ) 27 | { 28 | 29 | if (config.inst.indexOf('::') !== -1) { 30 | // *** form 2 or condensed form 3 31 | var filledCount = 0; 32 | for (index = 0, len = parts.length; index < len; ++index) { 33 | if (parts[index].length) { filledCount++; } 34 | } 35 | 36 | var missingCount; 37 | if (config.inst.indexOf('.') !== -1) { 38 | missingCount = 7 - filledCount; // condensed form 3 39 | } else { 40 | missingCount = 8 - filledCount; // form 2 41 | } 42 | 43 | var missingParts = new Array(missingCount); 44 | for (index = 0, len = missingParts.length; index < len; ++index) { 45 | missingParts[index] = '0'; 46 | } 47 | 48 | var replacement = ':' + missingParts.join(':') + ':'; 49 | var expanded = config.inst.replace('::', replacement); 50 | if (expanded[0] === ':') { expanded = expanded.slice(1); } 51 | if (expanded.slice(-1) === ':') { expanded = expanded.slice(0, -1); } 52 | 53 | if (expanded.match(core.FORMAT_REGEXPS.ipv6.form1)) { 54 | return errors; 55 | } else if (expanded.match(core.FORMAT_REGEXPS.ipv6.form3full)) { 56 | return errors; 57 | } 58 | } 59 | } 60 | } 61 | 62 | var desc = 'not a valid IPv6 address per RFC 2373 section 2.2'; 63 | errors.push(new Errors.FormatValidationError(config.resolutionScope, 64 | config.instanceContext, 'format', 'ipv6', config.inst, desc)); 65 | 66 | return errors; 67 | }; 68 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/formats/time.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // Format keyword 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../../errors.js') 6 | , core = require('../../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | var valid = false; 12 | 13 | var match = config.inst.match(core.FORMAT_REGEXPS.time); 14 | 15 | if (match) { 16 | var hour = parseInt(match[1], 10); 17 | var min = parseInt(match[2], 10); 18 | var sec = parseInt(match[3], 10); 19 | 20 | if ( 21 | hour >= 0 && hour <= 23 && 22 | min >= 0 && min <= 59 && 23 | sec >= 0 && sec <= 60 // it’s 60 during a leap second 24 | ) 25 | { 26 | valid = true; 27 | } 28 | } 29 | 30 | if (!valid) { 31 | var desc = 'not a valid time'; 32 | errors.push(new Errors.FormatValidationError(config.resolutionScope, 33 | config.instanceContext, 'format', 'time', config.inst, desc)); 34 | } 35 | 36 | return errors; 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/formats/uri.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // Format keyword 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../../errors.js') 6 | , core = require('../../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | 12 | if (!config.inst.match(core.FORMAT_REGEXPS.uri)) { 13 | var desc = 'not a valid URI'; 14 | errors.push(new Errors.FormatValidationError(config.resolutionScope, 15 | config.instanceContext, 'format', 'uri', config.inst, desc)); 16 | } 17 | 18 | return errors; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/items.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.3. Validation keywords for arrays 3 | // ****************************************************************** 4 | 5 | var testRunner = require('../index.js') 6 | ; 7 | 8 | module.exports = function(config) { 9 | var errors = []; 10 | var index, subTestConfig; 11 | 12 | if (Array.isArray(config.schema.items)) { 13 | // array of schemas for each item in the array 14 | var count = Math.min(config.inst.length, config.schema.items.length); 15 | for (index = 0; index < count; ++index) { 16 | var item = config.inst[index]; 17 | var itemSchema = config.schema.items[index]; 18 | 19 | subTestConfig = config.clone(); 20 | subTestConfig.inst = item; 21 | subTestConfig.schema = itemSchema; 22 | subTestConfig.resolutionScope = 23 | config.resolutionScope + '/items/' + index; 24 | subTestConfig.instanceContext = config.instanceContext + '/' + index; 25 | 26 | errors = errors.concat(testRunner(subTestConfig)); 27 | } 28 | 29 | // validate additional items in the array 30 | if (config.inst.length > config.schema.items.length && 31 | Object.prototype.hasOwnProperty.call(config.schema, 'additionalItems')) 32 | { 33 | // If additionalItems is boolean, validation for the 34 | // additionalItems keyword (above) is all we need. Otherwise, 35 | // validate each remaining item. 36 | if (typeof config.schema.additionalItems !== 'boolean') { 37 | for (index = config.schema.items.length; 38 | index < config.inst.length; 39 | ++index) 40 | { 41 | subTestConfig = config.clone(); 42 | subTestConfig.inst = config.inst[index]; 43 | subTestConfig.schema = config.schema.additionalItems; 44 | subTestConfig.resolutionScope = 45 | config.resolutionScope + '/items/' + index; 46 | subTestConfig.instanceContext = config.instanceContext + '/' + index; 47 | 48 | errors = errors.concat(testRunner(subTestConfig)); 49 | } 50 | } 51 | } 52 | 53 | } else { 54 | // one schema for all items in the array 55 | for (index = 0; index < config.inst.length; ++index) { 56 | subTestConfig = config.clone(); 57 | subTestConfig.inst = config.inst[index]; 58 | subTestConfig.schema = config.schema.items; 59 | subTestConfig.resolutionScope = config.resolutionScope + '/items'; 60 | subTestConfig.instanceContext = config.instanceContext + '/' + index; 61 | 62 | errors = errors.concat(testRunner(subTestConfig)); 63 | } 64 | } 65 | 66 | return errors; 67 | }; 68 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/maxItems.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.3. Validation keywords for arrays 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | if (config.inst.length > config.schema.maxItems) { 10 | errors.push(new Errors.ArrayValidationError(config.resolutionScope, 11 | config.instanceContext, 'maxItems', config.schema.maxItems, 12 | config.inst.length)); 13 | } 14 | return errors; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/maxLength.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.2. Validation keywords for strings 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | if (config.inst.length > config.schema.maxLength) { 10 | errors.push(new Errors.StringValidationError(config.resolutionScope, 11 | config.instanceContext, 'maxLength', config.schema.maxLength, 12 | config.inst.length)); 13 | } 14 | return errors; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/maxProperties.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.4. Validation keywords for objects 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | 10 | if (Object.keys(config.inst).length > config.schema.maxProperties) { 11 | errors.push(new Errors.ObjectValidationError(config.resolutionScope, 12 | config.instanceContext, 'maxProperties', config.schema.maxProperties, 13 | Object.keys(config.inst).length)); 14 | } 15 | 16 | return errors; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/maximum.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.1. Validation keywords for numeric instances 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | if (config.inst > config.schema.maximum) { 10 | errors.push(new Errors.NumericValidationError(config.resolutionScope, 11 | config.instanceContext, 'maximum', config.schema.maximum, config.inst)); 12 | } 13 | return errors; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/minItems.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.3. Validation keywords for arrays 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | if (config.inst.length < config.schema.minItems) { 10 | errors.push(new Errors.ArrayValidationError(config.resolutionScope, 11 | config.instanceContext, 'minItems', config.schema.minItems, 12 | config.inst.length)); 13 | } 14 | return errors; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/minLength.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.2. Validation keywords for strings 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | if (config.inst.length < config.schema.minLength) { 10 | errors.push(new Errors.StringValidationError(config.resolutionScope, 11 | config.instanceContext, 'minLength', config.schema.minLength, 12 | config.inst.length)); 13 | } 14 | return errors; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/minProperties.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.4. Validation keywords for objects 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | 10 | if (Object.keys(config.inst).length < config.schema.minProperties) { 11 | errors.push(new Errors.ObjectValidationError(config.resolutionScope, 12 | config.instanceContext, 'minProperties', config.schema.minProperties, 13 | Object.keys(config.inst).length)); 14 | } 15 | 16 | return errors; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/minimum.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.1. Validation keywords for numeric instances 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | if (config.inst < config.schema.minimum) { 10 | errors.push(new Errors.NumericValidationError(config.resolutionScope, 11 | config.instanceContext, 'minimum', config.schema.minimum, config.inst)); 12 | } 13 | return errors; 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/multipleOf.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.1. Validation keywords for numeric instances 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | var intermediate = config.inst / config.schema.multipleOf; 10 | if (intermediate % 1) { 11 | errors.push(new Errors.NumericValidationError(config.resolutionScope, 12 | config.instanceContext, 'multipleOf', config.schema.multipleOf, 13 | config.inst)); 14 | } 15 | return errors; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/not.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.5. Validation keywords for any instance type 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js') 6 | , testRunner = require('../index.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | 12 | var subTestConfig = config.clone(); 13 | subTestConfig.schema = config.schema.not; 14 | subTestConfig.resolutionScope = config.resolutionScope + '/not'; 15 | 16 | if (testRunner(subTestConfig).length === 0) { 17 | var desc = 'validates against this schema; must NOT validate against ' + 18 | 'this schema'; 19 | errors.push(new Errors.ValidationError(config.resolutionScope, 20 | config.instanceContext, 'not', config.schema.not, undefined, desc)); 21 | } 22 | return errors; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/oneOf.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.5. Validation keywords for any instance type 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js') 6 | , testRunner = require('../index.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = [], desc; 11 | var validAgainst = []; 12 | var subSchemaErrors = {}; 13 | 14 | for (var index = 0; index < config.schema.oneOf.length; ++index) { 15 | var subTestConfig = config.clone(); 16 | subTestConfig.schema = config.schema.oneOf[index]; 17 | subTestConfig.resolutionScope = config.resolutionScope + '/oneOf/' + index; 18 | 19 | var nestedErrors = testRunner(subTestConfig); 20 | if (nestedErrors.length === 0) { 21 | validAgainst.push(config.resolutionScope + '/oneOf/' + index); 22 | } else { 23 | var key = undefined; 24 | if (Object.prototype.hasOwnProperty.call(config.schema.oneOf[index], 25 | '$ref')) 26 | { 27 | key = config.schema.oneOf[index].$ref; 28 | } 29 | if (!key) { 30 | key = subTestConfig.schema.id || 'sub-schema-' + (index + 1); 31 | } 32 | subSchemaErrors[key] = nestedErrors; 33 | } 34 | } 35 | 36 | if (validAgainst.length !== 1) { 37 | if (validAgainst.length === 0) { 38 | desc = 'does not validate against any of these schemas'; 39 | } else { 40 | desc = 'validates against more than one of these schemas (' + 41 | validAgainst + ')'; 42 | } 43 | desc += '; must validate against one and only one of them'; 44 | 45 | errors.push(new Errors.SubSchemaValidationError(config.resolutionScope, 46 | config.instanceContext, 'oneOf', config.schema.oneOf, undefined, desc, 47 | validAgainst.length === 0 ? subSchemaErrors : undefined)); 48 | } 49 | 50 | return errors; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/pattern.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.2. Validation keywords for strings 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | if (!config.inst.match(new RegExp(config.schema.pattern))) { 10 | errors.push(new Errors.StringValidationError(config.resolutionScope, 11 | config.instanceContext, 'pattern', config.schema.pattern, config.inst)); 12 | } 13 | return errors; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/patternProperties.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.4. Validation keywords for objects 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | // no-op (_propertiesImpl will validate this) 9 | return []; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/properties.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.4. Validation keywords for objects 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | // no-op (_propertiesImpl will validate this) 9 | return []; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/required.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.4. Validation keywords for objects 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js'); 6 | 7 | module.exports = function(config) { 8 | var errors = []; 9 | 10 | var missing = []; 11 | for (var i = 0, len = config.schema.required.length; i !== len; ++i) { 12 | var prop = config.schema.required[i]; 13 | if (!Object.prototype.hasOwnProperty.call(config.inst, prop)) { 14 | missing.push(prop); 15 | } 16 | } 17 | 18 | if (missing.length) { 19 | errors.push(new Errors.ObjectValidationError(config.resolutionScope, 20 | config.instanceContext, 'required', config.schema.required, undefined, 21 | 'missing: ' + missing)); 22 | } 23 | 24 | return errors; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/type.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.5. Validation keywords for any instance type 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js') 6 | , core = require('../core.js') 7 | ; 8 | 9 | module.exports = function(config) { 10 | var errors = []; 11 | 12 | if (!Object.prototype.hasOwnProperty.call(config.schema, 'type')) { 13 | return errors; 14 | } 15 | 16 | var types = Array.isArray(config.schema.type) ? config.schema.type : 17 | [config.schema.type]; 18 | var instanceType = core.apparentType(config.inst); 19 | 20 | if (instanceType === 'integer') { 21 | if (types.indexOf('integer') === -1 && types.indexOf('number') === -1) { 22 | errors.push(new Errors.ValidationError(config.resolutionScope, 23 | config.instanceContext, 'type', config.schema.type, instanceType)); 24 | } 25 | } else { 26 | // boolean, string, number, null, array, object 27 | if (types.indexOf(instanceType) === -1) { 28 | errors.push(new Errors.ValidationError(config.resolutionScope, 29 | config.instanceContext, 'type', config.schema.type, instanceType)); 30 | } 31 | } 32 | 33 | return errors; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/suites/draft-04/keywords/uniqueItems.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // § 5.3. Validation keywords for arrays 3 | // ****************************************************************** 4 | 5 | var Errors = require('../../../errors.js') 6 | , core = require('../core.js') 7 | 8 | module.exports = function(config) { 9 | var errors = []; 10 | 11 | if (config.schema.uniqueItems === true) { 12 | for (var x = 0; x < config.inst.length; ++x) { 13 | var item = config.inst[x]; 14 | for (var y = x + 1; y < config.inst.length; ++y) { 15 | if (core.jsonEqual(item, config.inst[y])) { 16 | errors.push(new Errors.ArrayValidationError(config.resolutionScope, 17 | config.instanceContext, 'uniqueItems', true, undefined, 18 | 'failed at index ' + x)); 19 | break; 20 | } 21 | } 22 | } 23 | } 24 | 25 | return errors; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/uri.js: -------------------------------------------------------------------------------- 1 | // 2 | // Not a general URI parser. Just enough to meet our needs. 3 | // 4 | 5 | 'use strict'; 6 | 7 | var url = require('url') 8 | ; 9 | 10 | var URN_REGEX = /^([Uu][Rr][Nn]:[^#\?\/]+)(\/[^#\?]+)?([#].*)?$/; 11 | 12 | // ****************************************************************** 13 | // Parse a URI. Returns an object that can be passed to format(). 14 | // ****************************************************************** 15 | function parse(uri) { 16 | var result = { baseUri: '', fragment: '#' }; 17 | 18 | if (uri.slice(0, 4).toLowerCase() === 'urn:') { 19 | result.kind = 'urn'; 20 | var parts = uri.match(URN_REGEX); 21 | if (parts) { 22 | result.baseUri = parts[1] + (parts[2] || ''); 23 | result.host = parts[1]; 24 | if (parts[2]) { result.pathname = parts[2]; } 25 | if (parts[3]) { result.fragment = parts[3]; } 26 | } 27 | } else { 28 | result.kind = 'url'; 29 | var parsed = url.parse(uri); 30 | if (parsed.hash) { 31 | result.fragment = parsed.hash; 32 | delete parsed.hash; 33 | } 34 | result.absolute = 35 | (parsed.host && parsed.pathname && parsed.pathname[0] === '/') ? 36 | true : false; 37 | result.baseUri = url.format(parsed); 38 | } 39 | 40 | return result; 41 | } 42 | exports.parse = parse; 43 | 44 | // ****************************************************************** 45 | // Format an object (as returned by the parse method) into a URI. 46 | // ****************************************************************** 47 | function format(uriObj) { 48 | return uriObj.baseUri + uriObj.fragment; 49 | } 50 | exports.format = format; 51 | 52 | // ****************************************************************** 53 | // resolve a relative URI 54 | // ****************************************************************** 55 | function resolve(from, to) { 56 | var objFrom = parse(from); 57 | var objTo = parse(to); 58 | 59 | if (objFrom.kind === 'url' && objTo.kind === 'url') { 60 | return format(parse(url.resolve(from, to))); 61 | } 62 | 63 | if (objFrom.kind === 'urn') { 64 | // In our world all URNs are absolute (if they weren’t they’d be 65 | // seen as a non-absolute URL). 66 | 67 | if (objTo.kind === 'urn') { 68 | return format(objTo); 69 | } 70 | 71 | // "from" is a urn and "to" is a url 72 | if (objTo.kind === 'url' && objTo.absolute) { 73 | return format(objTo); 74 | } else { 75 | if (objFrom.pathname) { 76 | objFrom.pathname = url.resolve(objFrom.pathname, objTo.baseUri); 77 | } 78 | objFrom.baseUri = objFrom.host + (objFrom.pathname || ''); 79 | objFrom.fragment = objTo.fragment; 80 | return format(objFrom); 81 | } 82 | } else { 83 | // "from" is a url and "to" is a urn (!) 84 | return format(objTo); 85 | } 86 | } 87 | exports.resolve = resolve; 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Nate Silva (http://natesilva.com/)", 3 | "name": "jayschema", 4 | "description": "A comprehensive JSON Schema validator for Node.js", 5 | "version": "0.3.2", 6 | "homepage": "https://github.com/natesilva/jayschema", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/natesilva/jayschema.git" 10 | }, 11 | "bugs": "https://github.com/natesilva/jayschema/issues", 12 | "main": "./lib/jayschema.js", 13 | "keywords": [ 14 | "json", 15 | "schema", 16 | "validation" 17 | ], 18 | "engines": { 19 | "node": ">=0.6.6" 20 | }, 21 | "dependencies": { 22 | "when": "~3.4.6" 23 | }, 24 | "devDependencies": { 25 | "should": "~4.0.4", 26 | "mocha": "~1.21.4" 27 | }, 28 | "scripts": { 29 | "test": "./node_modules/.bin/mocha tests" 30 | }, 31 | "bin": { 32 | "jayschema": "./bin/validate.js" 33 | }, 34 | "license": "BSD-3-Clause" 35 | } 36 | -------------------------------------------------------------------------------- /tests/apparentType.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true */ 4 | 5 | 6 | var should = require('should') 7 | , schemaValidator = require('../lib/suites/draft-04') 8 | , core = require('../lib/suites/draft-04/core.js') 9 | ; 10 | 11 | describe('Core § 3.5 JSON Schema primitive types:', function() { 12 | describe('array:', function() { 13 | it('should return "array"', function() { 14 | core.apparentType([]).should.equal('array'); 15 | core.apparentType([15, 37, 'abcdefg']).should.equal('array'); 16 | }); 17 | 18 | it('should not return "array" for non-array types', function() { 19 | core.apparentType(true).should.not.equal('array'); 20 | core.apparentType(false).should.not.equal('array'); 21 | core.apparentType(0).should.not.equal('array'); 22 | core.apparentType(42).should.not.equal('array'); 23 | core.apparentType(42.1).should.not.equal('array'); 24 | core.apparentType(null).should.not.equal('array'); 25 | core.apparentType({foo: [1, 2, 3]}).should.not.equal('array'); 26 | core.apparentType('hello world').should.not.equal('array'); 27 | core.apparentType(undefined).should.not.equal('array'); 28 | }); 29 | }); 30 | 31 | describe('boolean:', function() { 32 | it('should return "boolean"', function() { 33 | core.apparentType(true).should.equal('boolean'); 34 | core.apparentType(false).should.equal('boolean'); 35 | core.apparentType(1 === 1).should.equal('boolean'); 36 | core.apparentType(1 !== 1).should.equal('boolean'); 37 | }); 38 | 39 | it ('should not return "boolean" for non-boolean types', function() { 40 | core.apparentType([true, false]).should.not.equal('boolean'); 41 | core.apparentType(0).should.not.equal('boolean'); 42 | core.apparentType(42).should.not.equal('boolean'); 43 | core.apparentType(42.1).should.not.equal('boolean'); 44 | core.apparentType(null).should.not.equal('boolean'); 45 | core.apparentType({foo: [1, 2]}).should.not.equal('boolean'); 46 | core.apparentType('hello world').should.not.equal('boolean'); 47 | core.apparentType(undefined).should.not.equal('boolean'); 48 | }); 49 | }); 50 | 51 | describe('integer:', function() { 52 | it('should return "integer"', function() { 53 | core.apparentType(42).should.equal('integer'); 54 | core.apparentType(42.0).should.equal('integer'); 55 | core.apparentType(0).should.equal('integer'); 56 | }); 57 | 58 | it ('should not return "integer" for non-integer types', function() { 59 | core.apparentType([true, false]).should.not.equal('integer'); 60 | core.apparentType(true).should.not.equal('integer'); 61 | core.apparentType(false).should.not.equal('integer'); 62 | core.apparentType(42.1).should.not.equal('integer'); 63 | core.apparentType(null).should.not.equal('integer'); 64 | core.apparentType({foo: [1, 2]}).should.not.equal('integer'); 65 | core.apparentType('hello world').should.not.equal('integer'); 66 | core.apparentType(undefined).should.not.equal('integer'); 67 | }); 68 | }); 69 | 70 | describe('number:', function() { 71 | it('should return "number"', function() { 72 | core.apparentType(42.1).should.equal('number'); 73 | }); 74 | 75 | it ('should not return "number" for non-number types', function() { 76 | core.apparentType([true, false]).should.not.equal('number'); 77 | core.apparentType(true).should.not.equal('number'); 78 | core.apparentType(false).should.not.equal('number'); 79 | core.apparentType(0).should.not.equal('number'); 80 | core.apparentType(42).should.not.equal('number'); 81 | core.apparentType(null).should.not.equal('number'); 82 | core.apparentType({foo: [1, 2, 3]}).should.not.equal('number'); 83 | core.apparentType('hello world').should.not.equal('number'); 84 | core.apparentType(undefined).should.not.equal('number'); 85 | }); 86 | }); 87 | 88 | describe('null:', function() { 89 | it('should return "null"', function() { 90 | core.apparentType(null).should.equal('null'); 91 | }); 92 | 93 | it ('should not return "null" for non-null types', function() { 94 | core.apparentType([true, false]).should.not.equal('null'); 95 | core.apparentType(true).should.not.equal('null'); 96 | core.apparentType(false).should.not.equal('null'); 97 | core.apparentType(0).should.not.equal('null'); 98 | core.apparentType(42).should.not.equal('null'); 99 | core.apparentType(42.1).should.not.equal('null'); 100 | core.apparentType({foo: [1, 2, 3]}).should.not.equal('null'); 101 | core.apparentType('hello world').should.not.equal('null'); 102 | core.apparentType(undefined).should.not.equal('null'); 103 | }); 104 | }); 105 | 106 | describe('object:', function() { 107 | it('should return "object"', function() { 108 | core.apparentType({}).should.equal('object'); 109 | core.apparentType({a: 1, b: 2}).should.equal('object'); 110 | core.apparentType({foo: {a: 1, b: 2}}).should.equal('object'); 111 | core.apparentType({bar: [13, 17, 42]}).should.equal('object'); 112 | }); 113 | 114 | it ('should not return "object" for non-object types', function() { 115 | core.apparentType([true, false]).should.not.equal('object'); 116 | core.apparentType(true).should.not.equal('object'); 117 | core.apparentType(false).should.not.equal('object'); 118 | core.apparentType(0).should.not.equal('object'); 119 | core.apparentType(42).should.not.equal('object'); 120 | core.apparentType(42.1).should.not.equal('object'); 121 | core.apparentType(null).should.not.equal('object'); 122 | core.apparentType('hello world').should.not.equal('object'); 123 | core.apparentType(undefined).should.not.equal('object'); 124 | }); 125 | 126 | it('should not mistake null or Array for "object"', function() { 127 | core.apparentType(null).should.not.equal('object'); 128 | core.apparentType(['a', 'b', 'c']).should.not.equal('object'); 129 | }); 130 | }); 131 | 132 | describe('string:', function() { 133 | it('should return "string"', function() { 134 | core.apparentType('hello world').should.equal('string'); 135 | }); 136 | 137 | it ('should not return "string" for non-string types', function() { 138 | core.apparentType([true, false]).should.not.equal('string'); 139 | core.apparentType(true).should.not.equal('string'); 140 | core.apparentType(false).should.not.equal('string'); 141 | core.apparentType(0).should.not.equal('string'); 142 | core.apparentType(42).should.not.equal('string'); 143 | core.apparentType(42.1).should.not.equal('string'); 144 | core.apparentType(null).should.not.equal('string'); 145 | core.apparentType({foo: 'hello'}).should.not.equal('string'); 146 | core.apparentType(undefined).should.not.equal('string'); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /tests/bootstrap.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true should:true */ 4 | 5 | 6 | var should = require('should') 7 | , JaySchema = require('../lib/jayschema.js') 8 | , v4Schema = require('../lib/suites/draft-04/json-schema-draft-v4.json') 9 | ; 10 | 11 | var schemaUrl = 12 | 'http://www.jayschema.org/test-targets/json-schema-draft-4.json#'; 13 | 14 | describe('JSON schema self-validation test:', function() { 15 | describe('validate meta-schema (synchronously):', function() { 16 | var jj = new JaySchema(); 17 | it('should self-validate the JSON Schema schema', function() { 18 | jj.validate(v4Schema, v4Schema).should.be.empty; 19 | }); 20 | }); 21 | 22 | describe('validate meta-schema (asynchronously):', function() { 23 | var jj = new JaySchema(JaySchema.loaders.http); 24 | it('should self-validate the JSON Schema schema', function(done) { 25 | jj.validate(v4Schema, {$ref: schemaUrl}, function(errs) { 26 | should.not.exist(errs); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /tests/customFormatHandlers.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true, it:true */ 4 | /*jshint -W110 */ 5 | 6 | var should = require('should') 7 | , JaySchema = require('../lib/jayschema.js') 8 | ; 9 | 10 | describe('Custom handlers for the "format" keyword:', function() { 11 | describe('phone-us:', function() { 12 | var js = new JaySchema(); 13 | 14 | var US_TEL_REGEXP = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/; 15 | js.addFormat('phone-us', function(inst, schema) { 16 | if (US_TEL_REGEXP.test(inst)) { return null; } 17 | return 'must be a US phone number'; 18 | }); 19 | 20 | var schema = { 21 | type: 'string', 22 | format: 'phone-us' 23 | }; 24 | 25 | it('should be a valid US phone number', function() { 26 | js.validate('212-555-4444', schema).should.be.empty; 27 | js.validate('2125554444', schema).should.be.empty; 28 | js.validate('(212) 555-4444', schema).should.be.empty; 29 | }); 30 | 31 | it('should not be a valid US phone number', function() { 32 | js.validate('212-555', schema).should.not.be.empty; 33 | js.validate('212554444', schema).should.not.be.empty; 34 | js.validate('555-4444', schema).should.not.be.empty; 35 | js.validate(2125554444, schema).should.not.be.empty; 36 | }); 37 | }); 38 | 39 | describe('uri (override built-in):', function() { 40 | var jj = new JaySchema(); 41 | 42 | var schema = { 43 | type: 'object', 44 | properties: { 45 | uri: { type: 'string', format: 'uri' } 46 | } 47 | }; 48 | 49 | var CUSTOM_URI_REGEXP = new RegExp( 50 | "^([a-z0-9+.-]+):(?://(?:((?:[a-z0-9-._~!$&'()*+,;=:]|%[0-9A-F]{2})*)@)" + 51 | "?((?:[a-z0-9-._~!$&'()*+,;=]|%[0-9A-F]{2})*)(?::(\\d*))?(/(?:[a-z0-9-." + 52 | "_~!$&'()*+,;=:@/]|%[0-9A-F]{2})*)?|(/?(?:[a-z0-9-._~!$&'()*+,;=:@]|%[0" + 53 | "-9A-F]{2})+(?:[a-z0-9-._~!$&'()*+,;=:@/]|%[0-9A-F]{2})*)?)(?:\\?((?:[a" + 54 | "-z0-9-._~!$&'()*+,;=:/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&'()*+" + 55 | ",;=:/?@]|%[0-9A-F]{2})*))?$"); 56 | 57 | jj.addFormat('uri', function(inst, schema) { 58 | if (CUSTOM_URI_REGEXP.test(inst)) { return null; } 59 | return 'must be a valid URI'; 60 | }); 61 | 62 | it('should be a valid URI using our custom format', function() { 63 | var instance = { 64 | uri: 'http://www.google.com/trends/explore#q=esquivalience' 65 | }; 66 | jj.validate(instance, schema).should.be.empty; 67 | }); 68 | 69 | it('should not be a valid URI using our custom format', function() { 70 | var instance = { 71 | uri: '/trends/explore#q=esquivalience' 72 | }; 73 | jj.validate(instance, schema).should.not.be.empty; 74 | }); 75 | 76 | it('should not be a valid URI using our custom format', function() { 77 | var instance = 42; 78 | jj.validate(instance, schema).should.not.be.empty; 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/customLoaders.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true */ 4 | 5 | 6 | var should = require('should') 7 | , JaySchema = require('../lib/jayschema.js') 8 | ; 9 | 10 | describe('Custom loaders:', function() { 11 | 12 | describe('pathological loader', function() { 13 | 14 | // “loads” schemas that are infinitely recursive 15 | var counter = 0; 16 | var pathologicalLoader = function(ref, callback) { 17 | callback(null, { '$ref': 'schema-' + counter }); 18 | ++counter; 19 | }; 20 | 21 | var js = new JaySchema(pathologicalLoader); 22 | 23 | it('should not be allowed to recurse indefinitely', function(done) { 24 | js.validate({}, { '$ref': 'foo' }, function(errs) { 25 | should.exist(errs); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('multiple simultaneous validation against a schema that needs to ' + 32 | 'be loaded', function() 33 | { 34 | // waits, and then “loads” a schema 35 | var slowLoader = function(ref, callback) { 36 | setTimeout(function() { 37 | var schema = { 'type': 'integer', 'multipleOf': 8 }; 38 | callback(null, schema); 39 | }, 100); 40 | }; 41 | 42 | var js = new JaySchema(slowLoader); 43 | 44 | it('should work', function(done) { 45 | var schema = { '$ref': 'http://foo.bar/baz#' }; 46 | 47 | var instance1 = 64; 48 | var instance2 = 808; 49 | 50 | var counter = 2; 51 | 52 | js.validate(instance1, schema, function(errs) { 53 | should.not.exist(errs); 54 | counter--; 55 | if (counter === 0) { done(); } 56 | }); 57 | 58 | js.validate(instance2, schema, function(errs) { 59 | should.not.exist(errs); 60 | counter--; 61 | if (counter === 0) { done(); } 62 | }); 63 | }); 64 | }); 65 | 66 | describe('passing schema url directly to validate function should work', 67 | function () 68 | { 69 | it('should work', function (done) { 70 | var schema = { 'type': 'integer', 'multipleOf': 8 }; 71 | 72 | var loader = function (ref, callback) { 73 | process.nextTick(function () { 74 | ref.should.equal('homemadescheme://host.com/path'); 75 | callback(null, schema); 76 | }); 77 | }; 78 | 79 | var js = new JaySchema(loader); 80 | 81 | var instance = 64; 82 | 83 | js.validate(instance, 'homemadescheme://host.com/path', function(errs) { 84 | should.not.exist(errs); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('.. is legal in a url', function (done) { 90 | var schema = { 'type': 'integer', 'multipleOf': 8 }; 91 | 92 | var loader = function (ref, callback) { 93 | process.nextTick(function () { 94 | ref.should.equal('homemadescheme://host.com/../path'); 95 | callback(null, schema); 96 | }); 97 | }; 98 | 99 | var js = new JaySchema(loader); 100 | 101 | var instance = 64; 102 | 103 | js.validate(instance, 'homemadescheme://host.com/../path', function(errs) 104 | { 105 | should.not.exist(errs); 106 | done(); 107 | }); 108 | }); 109 | }); 110 | 111 | }); 112 | -------------------------------------------------------------------------------- /tests/downloadSchema.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true */ 4 | 5 | 6 | var assert = require('assert') 7 | , httpLoader = require('../lib/httpLoader.js') 8 | ; 9 | 10 | describe('GET request wrapper:', 11 | function() 12 | { 13 | describe('retrieve JSON Schema Draft V4 schema:', function() { 14 | 15 | it('should retrieve the schema', function(done) { 16 | var url = 17 | 'http://www.jayschema.org/test-targets/json-schema-draft-4.json#'; 18 | httpLoader(url, function(err, schema) { 19 | if (err) { throw err; } 20 | assert.equal('http://json-schema.org/draft-04/schema#', schema.id); 21 | done(); 22 | }); 23 | }); 24 | 25 | it('should follow 3xx redirects to retrieve a schema', function(done) { 26 | var url = 'http://draft-4-redirect.jayschema.org/#'; 27 | httpLoader(url, function(err, schema) { 28 | if (err) { throw err; } 29 | assert.equal('http://json-schema.org/draft-04/schema#', schema.id); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should retrieve a schema over HTTPS (SSL)', function(done) { 35 | var url = 'https://storage.googleapis.com/www.jayschema.org/' + 36 | 'test-targets/json-schema-draft-4.json#'; 37 | httpLoader(url, function(err, schema) { 38 | if (err) { throw err; } 39 | assert.equal('http://json-schema.org/draft-04/schema#', schema.id); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should fail to retrieve the URL', function(done) { 45 | var url = 'http://www.jayschema.org/test-targets/this-does-not-exist'; 46 | httpLoader(url, function(err) { 47 | assert(err); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('should fail to get a schema, even though the URL is valid', 53 | function(done) 54 | { 55 | var url = 'http://www.jayschema.org/test-targets/not-a-schema.html'; 56 | httpLoader(url, function(err) { 57 | assert(err); 58 | done(); 59 | }); 60 | }); 61 | 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /tests/equality.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true */ 4 | 5 | 6 | var should = require('should') 7 | , schemaValidator = require('../lib/suites/draft-04') 8 | , core = require('../lib/suites/draft-04/core.js') 9 | ; 10 | 11 | describe('Core § 3.6 JSON value equality:', function() { 12 | describe('null:', function() { 13 | it('should be equal', function() { 14 | core.jsonEqual(null, null).should.be.true; 15 | }); 16 | 17 | it('should not be equal', function() { 18 | core.jsonEqual(null, undefined).should.be.false; 19 | core.jsonEqual(null, {}).should.be.false; 20 | core.jsonEqual(null, 0).should.be.false; 21 | core.jsonEqual(null, false).should.be.false; 22 | core.jsonEqual(null, '').should.be.false; 23 | }); 24 | }); 25 | 26 | describe('boolean:', function() { 27 | it('should be equal', function() { 28 | core.jsonEqual(true, true).should.be.true; 29 | core.jsonEqual(false, false).should.be.true; 30 | }); 31 | 32 | it('should not be equal', function() { 33 | core.jsonEqual(true, false).should.be.false; 34 | core.jsonEqual(false, true).should.be.false; 35 | core.jsonEqual(true, {}).should.be.false; 36 | core.jsonEqual(true, 0).should.be.false; 37 | core.jsonEqual(true, '').should.be.false; 38 | }); 39 | }); 40 | 41 | describe('string:', function() { 42 | it('should be equal', function() { 43 | core.jsonEqual('hello', 'hello').should.be.true; 44 | }); 45 | 46 | it('should not be equal', function() { 47 | core.jsonEqual('hello', 'goodbye').should.be.false; 48 | core.jsonEqual('hello', {}).should.be.false; 49 | core.jsonEqual('hello', 0).should.be.false; 50 | core.jsonEqual('hello', '').should.be.false; 51 | core.jsonEqual('0', 0).should.be.false; 52 | }); 53 | }); 54 | 55 | describe('number:', function() { 56 | it('should be equal', function() { 57 | core.jsonEqual(17, 17).should.be.true; 58 | core.jsonEqual(17, 17.0).should.be.true; 59 | core.jsonEqual(3.14195, 3.14195).should.be.true; 60 | core.jsonEqual(1/3, 1/3).should.be.true; 61 | }); 62 | 63 | it('should not be equal', function() { 64 | core.jsonEqual(7, '7').should.be.false; 65 | core.jsonEqual(0, false).should.be.false; 66 | core.jsonEqual(42.1, 42.2).should.be.false; 67 | core.jsonEqual(42.1, 42).should.be.false; 68 | }); 69 | }); 70 | 71 | describe('array:', function() { 72 | it('should be equal', function() { 73 | core.jsonEqual([], []).should.be.true; 74 | core.jsonEqual(['a', 'b', 'c'], ['a', 'b', 'c']) 75 | .should.be.true; 76 | 77 | core.jsonEqual( 78 | ['a', 'b', {foo: 'bar', baz: 42}], 79 | ['a', 'b', {baz: 42, foo: 'bar'}] 80 | ).should.be.true; 81 | }); 82 | 83 | it('should not be equal', function() { 84 | core.jsonEqual(['a', 'b', 'c'], ['a', 'c', 'b']) 85 | .should.not.be.true; 86 | core.jsonEqual(['a', 'b', 'c'], ['a', 'b']) 87 | .should.not.be.true; 88 | core.jsonEqual(['a', 'b'], ['a', 'b', 'c']) 89 | .should.not.be.true; 90 | core.jsonEqual( 91 | ['a', 'b', {foo: 'bar', baz: 42}], 92 | ['a', 'b', {baz: 42, foo: 'bar', x:10}] 93 | ).should.not.be.true; 94 | core.jsonEqual(['a', 'b'], {}).should.not.be.true; 95 | }); 96 | }); 97 | 98 | 99 | describe('object:', function() { 100 | it('should be equal', function() { 101 | core.jsonEqual({}, {}).should.be.true; 102 | core.jsonEqual({foo: 'bar', baz: 42}, {foo: 'bar', baz: 42}) 103 | .should.be.true; 104 | core.jsonEqual({foo: 'bar', baz: 42}, {baz: 42, foo: 'bar'}) 105 | .should.be.true; 106 | 107 | core.jsonEqual( 108 | {id: 1, posts: [37, 42], user: {name: 'Fred', friends: 55}}, 109 | {user: {friends: 55, name: 'Fred'}, posts: [37, 42], id: 1} 110 | ).should.be.true; 111 | }); 112 | 113 | it('should not be equal', function() { 114 | core.jsonEqual({}, {age: 42}).should.not.be.true; 115 | core.jsonEqual({}, null).should.not.be.true; 116 | core.jsonEqual( 117 | {foo: 'bar', baz: 42}, 118 | {foo: 'bar', baz: 42, qux: 37} 119 | ).should.not.be.true; 120 | core.jsonEqual( 121 | {id: 1, posts: [37, 42], user: {name: 'Fred', friends: 55}}, 122 | {user: {friends: 55, name: 'Fred'}, posts: [37, 42], id: 2} 123 | ).should.not.be.true; 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /tests/errors.js: -------------------------------------------------------------------------------- 1 | var Errors = require('../lib/errors.js') 2 | , JaySchema = require('../lib/jayschema.js') 3 | , should = require('should'); 4 | 5 | describe("Errors", function() { 6 | describe("falsy values", function() { 7 | it("should set all values", function() { 8 | var error = new Errors.ValidationError(0, 0, 0, 0, 0, 0); 9 | 10 | error.resolutionScope.should.equal(0); 11 | error.instanceContext.should.equal(0); 12 | error.constraintName.should.equal(0); 13 | error.constraintValue.should.equal(0); 14 | error.testedValue.should.equal(0); 15 | }); 16 | 17 | it("should set constraintValue", function() { 18 | var jj = new JaySchema(); 19 | var result = jj.validate(1, {maximum: 0}); 20 | result[0].constraintValue.should.equal(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path'); 3 | 4 | function getTests(dir) { 5 | var dirEntries = fs.readdirSync(dir); 6 | 7 | var files = []; 8 | var dirs = []; 9 | 10 | dirEntries.forEach(function(entry) { 11 | var fullPath = path.join(dir, entry); 12 | var stats = fs.statSync(fullPath); 13 | if (stats.isDirectory()) { 14 | dirs.push(fullPath); 15 | } else if (stats.isFile()) { 16 | if (path.extname(entry) === '.json') { 17 | files.push(fullPath); 18 | } 19 | } 20 | }); 21 | 22 | dirs.forEach(function(dir) { 23 | files = files.concat(getTests(dir)); 24 | }); 25 | 26 | return files; 27 | } 28 | exports.getTests = getTests; 29 | 30 | function shouldSkip(jsonFile, testGroup, test, BLACKLISTED_TESTS) { 31 | var basename = path.basename(jsonFile); 32 | if (basename in BLACKLISTED_TESTS) { 33 | var items = BLACKLISTED_TESTS[basename]; 34 | if ('*' in items) { return true; } 35 | if (testGroup in items) { 36 | if ('*' in items[testGroup] || test in items[testGroup]) { 37 | return true; 38 | } 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | exports.shouldSkip = shouldSkip; 45 | -------------------------------------------------------------------------------- /tests/json-schema-test-suite-v3.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | // 3 | // Execute tests defined in the JSON Schema Test Suite from: 4 | // https://github.com/json-schema/JSON-Schema-Test-Suite 5 | // 6 | // At the time of this writing, the tests are against JSON Schema 7 | // Draft v3 (we support v4). Therefore some of the tests are not 8 | // applicable, or fail due to specification changes. Those tests are 9 | // are blacklisted and skipped below. 10 | 11 | /*global describe:true it:true */ 12 | 13 | 14 | var should = require('should') 15 | , JaySchema = require('../lib/jayschema.js') 16 | , fs = require('fs') 17 | , path = require('path') 18 | , helpers = require("./helpers.js") 19 | ; 20 | 21 | // support Node 0.6.x 22 | var existsSync = fs.existsSync || path.existsSync; 23 | 24 | var BLACKLISTED_TESTS = { 25 | 26 | 'format.json': { 27 | '*': '"format": "regex" not in v4 (use the "pattern" keyword instead)' 28 | }, 29 | 30 | 'dependencies.json': { 31 | dependencies: { 32 | '*': 'dependency values must be array or object in v4 (§ 5.4.5.1)' 33 | } 34 | }, 35 | 36 | 'disallow.json': { 37 | '*': 'disallow keyword removed from v4' 38 | }, 39 | 40 | 'divisibleBy.json': { 41 | '*': 'divisibleBy keyword removed from v4 (see multipleOf)' 42 | }, 43 | 44 | 'required.json': { 45 | '*': '"required" works differently in v4' 46 | }, 47 | 48 | 'type.json': { 49 | 'any type matches any type': { 50 | '*': 'the "any" type is not in v4' 51 | }, 52 | 53 | 'integer type matches integers': { 54 | 'a float is not an integer even without fractional part': 55 | 'no longer enforced in v4' 56 | }, 57 | 58 | 'types can include schemas': { 59 | '*': 'types cannot include schemas in v4' 60 | }, 61 | 62 | 'when types includes a schema it should fully validate the schema': { 63 | '*': 'types cannot include schemas in v4' 64 | }, 65 | 66 | 'types from separate schemas are merged': { 67 | '*': 'types cannot include schemas in v4' 68 | } 69 | }, 70 | 71 | 'extends.json': { 72 | '*': 'extends keyword removed from v4' 73 | }, 74 | 75 | 'jsregex.json': { 76 | '*': 'feature not in v4 (use the "pattern" keyword instead)' 77 | }, 78 | 79 | 'zeroTerminatedFloats.json': { 80 | '*': 'no longer enforced in v4' 81 | 82 | }, 83 | 84 | 'ref.json': { 85 | 'remote ref, containing refs itself': { 86 | '*': 'not testing remote refs in draft3 (we do the draft4 version of ' + 87 | 'this test)' 88 | } 89 | } 90 | }; 91 | 92 | describe('JSON Schema Test Suite:', function() { 93 | 94 | var testPath = path.join(__dirname, 'JSON-Schema-Test-Suite', 'tests', 95 | 'draft3'); 96 | 97 | it('should find the JSON-Schema-Test-Suite tests (do `git submodule init; ' + 98 | 'git submodule update` to include them)', function() 99 | { 100 | existsSync(testPath).should.be.true; 101 | }); 102 | 103 | if (!existsSync(testPath)) { return; } 104 | 105 | var files = helpers.getTests(testPath); 106 | 107 | for (var index = 0, len = files.length; index !== len; ++index) { 108 | var jsonFile = files[index]; 109 | var testGroups = require(jsonFile); 110 | 111 | testGroups.forEach(function(group) { 112 | describe(path.relative('.', jsonFile) + '|' + group.description + ':', 113 | function() 114 | { 115 | group.tests.forEach(function(test) { 116 | 117 | if (!helpers.shouldSkip(jsonFile, group.description, test.description, BLACKLISTED_TESTS)) { 118 | it(test.description, function() { 119 | var jj = new JaySchema(); 120 | var result = jj.validate(test.data, group.schema); 121 | if (test.valid) { 122 | result.should.be.empty; 123 | } else { 124 | result.should.not.be.empty; 125 | } 126 | }); 127 | } 128 | 129 | }, this); 130 | }); 131 | }, this); 132 | 133 | } 134 | }); 135 | -------------------------------------------------------------------------------- /tests/json-schema-test-suite-v4.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | // 3 | // Execute Draft v4 tests defined in the JSON Schema Test Suite 4 | // from: https://github.com/json-schema/JSON-Schema-Test-Suite 5 | 6 | /*global describe:true it:true */ 7 | 8 | 9 | var should = require('should') 10 | , JaySchema = require('../lib/jayschema.js') 11 | , fs = require('fs') 12 | , path = require('path') 13 | , helpers = require('./helpers.js') 14 | ; 15 | 16 | // support Node 0.6.x 17 | var existsSync = fs.existsSync || path.existsSync; 18 | 19 | var BLACKLISTED_TESTS = { 20 | 21 | 'zeroTerminatedFloats.json': { 22 | '*': 'optional feature that can\'t be implemented in JavaScript' 23 | }, 24 | 25 | 'format.json': { 26 | 'validation of regular expressions': { 27 | '*': 'Draft v3 feature removed from Draft v4, incorrectly remains in ' + 28 | 'tests' 29 | } 30 | }, 31 | 32 | 'jsregex.json': { 33 | '*': 'Draft v3 feature removed from Draft v4, incorrectly remains in tests' 34 | } 35 | 36 | }; 37 | 38 | describe('JSON Schema Test Suite:', function() { 39 | 40 | var testPath = path.join(__dirname, 'JSON-Schema-Test-Suite', 'tests', 41 | 'draft4'); 42 | 43 | it('should find the JSON-Schema-Test-Suite tests (do `git submodule init; ' + 44 | 'git submodule update` to include them)', function() 45 | { 46 | existsSync(testPath).should.be.true; 47 | }); 48 | 49 | if (!existsSync(testPath)) { return; } 50 | 51 | var files = helpers.getTests(testPath); 52 | 53 | for (var index = 0, len = files.length; index !== len; ++index) { 54 | var jsonFile = files[index]; 55 | var testGroups = require(jsonFile); 56 | 57 | testGroups.forEach(function(group) { 58 | describe(path.relative('.', jsonFile) + '|' + group.description + ':', 59 | function() 60 | { 61 | group.tests.forEach(function(test) { 62 | 63 | if (!helpers.shouldSkip(jsonFile, group.description, test.description, BLACKLISTED_TESTS)) { 64 | it(test.description, function() { 65 | var jj = new JaySchema(); 66 | var result = jj.validate(test.data, group.schema); 67 | if (test.valid) { 68 | result.should.be.empty; 69 | } else { 70 | result.should.not.be.empty; 71 | } 72 | }); 73 | } 74 | 75 | }, this); 76 | }); 77 | }, this); 78 | 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /tests/our-tests-async.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true */ 4 | 5 | 6 | var should = require('should') 7 | , JaySchema = require('../lib/jayschema.js') 8 | , path = require('path') 9 | , helpers = require('./helpers.js') 10 | ; 11 | 12 | var BLACKLISTED_TESTS = { 13 | 14 | 'refsSync.json': { 15 | '*': 'these tests only apply in non-async mode' 16 | } 17 | 18 | }; 19 | 20 | describe('Our test suite (running async):', function() { 21 | 22 | var files = helpers.getTests(path.join(__dirname, 'our-tests')); 23 | 24 | for (var index = 0, len = files.length; index !== len; ++index) { 25 | var jsonFile = files[index]; 26 | var testGroups = require(jsonFile); 27 | 28 | testGroups.forEach(function(group) { 29 | describe(path.relative('.', jsonFile) + '|' + group.description + ':', 30 | function() 31 | { 32 | group.tests.forEach(function(test) { 33 | 34 | if (!helpers.shouldSkip(jsonFile, group.description, test.description, BLACKLISTED_TESTS)) { 35 | it(test.description, function(done) { 36 | var jj = new JaySchema(JaySchema.loaders.http); 37 | jj.validate(test.data, group.schema, function(errs) { 38 | if (test.valid) { 39 | should.not.exist(errs); 40 | } else { 41 | errs.should.not.be.empty; 42 | } 43 | done(); 44 | }); 45 | }); 46 | } 47 | 48 | }, this); 49 | }); 50 | }, this); 51 | 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /tests/our-tests-no-hasOwnProperty.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | // 3 | // These tests ensure that JaySchema works with instances that are 4 | // not derived from Object. 5 | 6 | /*global describe:true it:true */ 7 | 8 | 9 | var should = require('should') 10 | , JaySchema = require('../lib/jayschema.js') 11 | , path = require('path') 12 | , helpers = require('./helpers.js') 13 | ; 14 | 15 | 16 | var BLACKLISTED_TESTS = { 17 | 18 | 'refsAsync.json': { 19 | '*': 'these tests only apply in async mode' 20 | } 21 | 22 | }; 23 | 24 | function duplicateWithNullPrototype(src) { 25 | if (!(src instanceof Object)) { return src; } 26 | 27 | if (Array.isArray(src)) { 28 | return Array.prototype.map.call(src, duplicateWithNullPrototype); 29 | } 30 | 31 | var dest = Object.create(null); 32 | Object.keys(src).forEach(function(key) { 33 | if (src[key] instanceof Object) { 34 | dest[key] = duplicateWithNullPrototype(src[key]); 35 | } else { 36 | dest[key] = src[key]; 37 | } 38 | }); 39 | return dest; 40 | } 41 | 42 | describe('Our test suite (running synchronously, instances without ' + 43 | 'Object.prototype):', function() 44 | { 45 | 46 | var files = helpers.getTests(path.join(__dirname, 'our-tests')); 47 | 48 | for (var index = 0, len = files.length; index !== len; ++index) { 49 | var jsonFile = files[index]; 50 | var testGroups = require(jsonFile); 51 | 52 | testGroups.forEach(function(group) { 53 | describe(path.relative('.', jsonFile) + '|' + group.description + ':', 54 | function() 55 | { 56 | var nullPrototypeSchema = duplicateWithNullPrototype(group.schema); 57 | 58 | group.tests.forEach(function(test) { 59 | 60 | if (!helpers.shouldSkip(jsonFile, group.description, test.description, BLACKLISTED_TESTS)) { 61 | it(test.description, function() { 62 | var jj = new JaySchema(); 63 | var result = jj.validate(duplicateWithNullPrototype(test.data), 64 | nullPrototypeSchema); 65 | if (test.valid) { 66 | result.should.be.empty; 67 | } else { 68 | result.should.not.be.empty; 69 | } 70 | }); 71 | } 72 | 73 | }, this); 74 | }); 75 | }, this); 76 | 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /tests/our-tests.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true */ 4 | 5 | 6 | var should = require('should') 7 | , JaySchema = require('../lib/jayschema.js') 8 | , path = require('path') 9 | , helpers = require('./helpers.js') 10 | ; 11 | 12 | 13 | var BLACKLISTED_TESTS = { 14 | 15 | 'refsAsync.json': { 16 | '*': 'these tests only apply in async mode' 17 | } 18 | 19 | }; 20 | 21 | describe('Our test suite (running synchronously):', function() { 22 | 23 | var files = helpers.getTests(path.join(__dirname, 'our-tests')); 24 | 25 | for (var index = 0, len = files.length; index !== len; ++index) { 26 | var jsonFile = files[index]; 27 | var testGroups = require(jsonFile); 28 | 29 | testGroups.forEach(function(group) { 30 | describe(path.relative('.', jsonFile) + '|' + group.description + ':', 31 | function() 32 | { 33 | group.tests.forEach(function(test) { 34 | 35 | if (!helpers.shouldSkip(jsonFile, group.description, test.description, BLACKLISTED_TESTS)) { 36 | it(test.description, function() { 37 | var jj = new JaySchema(); 38 | var result = jj.validate(test.data, group.schema); 39 | if (test.valid) { 40 | result.should.be.empty; 41 | } else { 42 | result.should.not.be.empty; 43 | } 44 | }); 45 | } 46 | 47 | }, this); 48 | }); 49 | }, this); 50 | 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /tests/our-tests/arrays.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "additionalItems true", 4 | "schema": { 5 | "type": "array", 6 | "additionalItems": true 7 | }, 8 | "tests": [ 9 | { 10 | "description": "matches schema", 11 | "data": [1, 2, 3], 12 | "valid": true 13 | } 14 | ] 15 | }, 16 | 17 | { 18 | "description": "additionalItems false", 19 | "schema": { 20 | "type": "array", 21 | "additionalItems": false 22 | }, 23 | "tests": [ 24 | { 25 | "description": "matches schema because 'items' is not specified", 26 | "data": [1, 2, 3], 27 | "valid": true 28 | } 29 | ] 30 | }, 31 | 32 | { 33 | "description": "additionalItems: false and items as an array", 34 | "schema": { 35 | "type": "array", 36 | "items": [ {}, {}, {} ], 37 | "additionalItems": false 38 | }, 39 | "tests": [ 40 | { 41 | "description": "empty array matches schema", 42 | "data": [], 43 | "valid": true 44 | }, 45 | { 46 | "description": "array of arrays matches schema", 47 | "data": [[ 1, 2, 3, 4 ], [ 5, 6, 7, 8 ]], 48 | "valid": true 49 | }, 50 | { 51 | "description": "array of 3 elements matches schema", 52 | "data": [1, 2, 3], 53 | "valid": true 54 | }, 55 | { 56 | "description": "array of 4 elements does not match schema", 57 | "data": [1, 2, 3, 4], 58 | "valid": false 59 | } 60 | ] 61 | }, 62 | 63 | { 64 | "description": "additionalItems as a schema", 65 | "schema": { 66 | "type": "array", 67 | "additionalItems": { "type": "string" } 68 | }, 69 | "tests": [ 70 | { 71 | "description": "matches schema", 72 | "data": ["a", "b", "c"], 73 | "valid": true 74 | } 75 | ] 76 | }, 77 | 78 | { 79 | "description": "additionalItems as a schema", 80 | "schema": { 81 | "type": "array", 82 | "additionalItems": { "type": "string" } 83 | }, 84 | "tests": [ 85 | { 86 | "description": "matches schema", 87 | "data": ["a", "b", "c"], 88 | "valid": true 89 | } 90 | ] 91 | }, 92 | 93 | { 94 | "description": "items as an array and additionalItems true", 95 | "schema": { 96 | "type": "array", 97 | "items": [ {"type": "number"}, {"type": "integer"} ], 98 | "additionalItems": true 99 | }, 100 | "tests": [ 101 | { 102 | "description": "matches schema", 103 | "data": [1, 2, 3], 104 | "valid": true 105 | }, 106 | { 107 | "description": "does not match schema", 108 | "data": [1, "2", 3], 109 | "valid": false 110 | } 111 | ] 112 | }, 113 | 114 | { 115 | "description": "items as an array and additionalItems false", 116 | "schema": { 117 | "type": "array", 118 | "items": [ {"type": "number"}, {"type": "integer"} ], 119 | "additionalItems": false 120 | }, 121 | "tests": [ 122 | { 123 | "description": "matches schema", 124 | "data": [1, 2], 125 | "valid": true 126 | }, 127 | { 128 | "description": "does not match schema", 129 | "data": [1, 2, 3], 130 | "valid": false 131 | } 132 | ] 133 | }, 134 | 135 | { 136 | "description": "maxItems", 137 | "schema": { 138 | "type": "array", 139 | "maxItems": 3 140 | }, 141 | "tests": [ 142 | { 143 | "description": "matches schema", 144 | "data": [1, 2], 145 | "valid": true 146 | }, 147 | { 148 | "description": "max limit matches schema", 149 | "data": [1, 2, 3], 150 | "valid": true 151 | }, 152 | { 153 | "description": "does not match schema", 154 | "data": [1, 2, 3, 4], 155 | "valid": false 156 | } 157 | ] 158 | }, 159 | 160 | { 161 | "description": "minItems", 162 | "schema": { 163 | "type": "array", 164 | "minItems": 3 165 | }, 166 | "tests": [ 167 | { 168 | "description": "matches schema", 169 | "data": [1, 2, 3, 4], 170 | "valid": true 171 | }, 172 | { 173 | "description": "min limit matches schema", 174 | "data": [1, 2, 3], 175 | "valid": true 176 | }, 177 | { 178 | "description": "does not match schema", 179 | "data": [1, 2], 180 | "valid": false 181 | } 182 | ] 183 | }, 184 | 185 | { 186 | "description": "uniqueItems", 187 | "schema": { 188 | "type": "array", 189 | "uniqueItems": true 190 | }, 191 | "tests": [ 192 | { 193 | "description": "matches schema", 194 | "data": [1, 2, 3], 195 | "valid": true 196 | }, 197 | { 198 | "description": "string is not a number, so matches schema", 199 | "data": [1, 2, 3, "3"], 200 | "valid": true 201 | }, 202 | { 203 | "description": "does not match schema", 204 | "data": [1, 2, 3, 3], 205 | "valid": false 206 | } 207 | ] 208 | }, 209 | 210 | { 211 | "description": "uniqueItems explicitly set to false", 212 | "schema": { 213 | "type": "array", 214 | "uniqueItems": false 215 | }, 216 | "tests": [ 217 | { 218 | "description": "matches schema even though all items are unique", 219 | "data": [1, 2, 3], 220 | "valid": true 221 | }, 222 | { 223 | "description": "matches schema", 224 | "data": [1, 2, 3, "3"], 225 | "valid": true 226 | } 227 | ] 228 | }, 229 | 230 | { 231 | "description": "items as a schema", 232 | "schema": { 233 | "type": "array", 234 | "items": { "type": "integer" } 235 | }, 236 | "tests": [ 237 | { 238 | "description": "matches schema", 239 | "data": [1, 2, 3], 240 | "valid": true 241 | }, 242 | { 243 | "description": "float element not match schema", 244 | "data": [1, 2, 3, 4.2], 245 | "valid": false 246 | }, 247 | { 248 | "description": "string element not match schema", 249 | "data": [1, 2, 3, "4"], 250 | "valid": false 251 | } 252 | ] 253 | }, 254 | 255 | { 256 | "description": "items as an array and additionalItems false", 257 | "schema": { 258 | "type": "array", 259 | "items": [{"type": "integer"}, {"type": "string"}, {"type": "object"}], 260 | "additionalItems": false 261 | }, 262 | "tests": [ 263 | { 264 | "description": "matches schema", 265 | "data": [1, "2", {"a": 3}], 266 | "valid": true 267 | }, 268 | { 269 | "description": "second element does not match schema", 270 | "data": [1, 2, {"a": 3}], 271 | "valid": false 272 | } 273 | ] 274 | }, 275 | 276 | { 277 | "description": "items as an array and additionalItems true", 278 | "schema": { 279 | "type": "array", 280 | "items": [{"type": "integer"}, {"type": "string"}, {"type": "object"}], 281 | "additionalItems": true 282 | }, 283 | "tests": [ 284 | { 285 | "description": "matches schema", 286 | "data": [1, "2", {"a": 3}], 287 | "valid": true 288 | }, 289 | { 290 | "description": "with additional item matches schema", 291 | "data": [1, "2", {"a": 3}, 4], 292 | "valid": true 293 | } 294 | ] 295 | }, 296 | 297 | 298 | { 299 | "description": "items as an array and additionalItems as a schema", 300 | "schema": { 301 | "type": "array", 302 | "items": [{"type": "integer"}, {"type": "string"}, {"type": "object"}], 303 | "additionalItems": {"type": "array"} 304 | }, 305 | "tests": [ 306 | { 307 | "description": "matches schema (additional items not required)", 308 | "data": [1, "2", {"a": 3}], 309 | "valid": true 310 | }, 311 | { 312 | "description": "with additional item that matches schema", 313 | "data": [1, "2", {"a": 3}, [4]], 314 | "valid": true 315 | }, 316 | { 317 | "description": "with additional item that does not match schema", 318 | "data": [1, "2", {"a": 3}, 4], 319 | "valid": false 320 | } 321 | ] 322 | } 323 | ] 324 | -------------------------------------------------------------------------------- /tests/our-tests/numeric.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "multipleOf", 4 | "schema": { 5 | "type": "integer", 6 | "multipleOf": 8 7 | }, 8 | "tests": [ 9 | { 10 | "description": "integer matches schema", 11 | "data": 64, 12 | "valid": true 13 | }, 14 | { 15 | "description": "integer does not match schema", 16 | "data": 65, 17 | "valid": false 18 | } 19 | ] 20 | }, 21 | 22 | { 23 | "description": "multipleOf for non-integer numbers", 24 | "schema": { 25 | "type": "number", 26 | "multipleOf": 42.1 27 | }, 28 | "tests": [ 29 | { 30 | "description": "number matches schema", 31 | "data": 84.2, 32 | "valid": true 33 | }, 34 | { 35 | "description": "number does not match schema", 36 | "data": 84.3, 37 | "valid": false 38 | } 39 | ] 40 | }, 41 | 42 | { 43 | "description": "maximum", 44 | "schema": { 45 | "type": "number", 46 | "maximum": 42 47 | }, 48 | "tests": [ 49 | { 50 | "description": "negative integer matches schema", 51 | "data": -100, 52 | "valid": true 53 | }, 54 | { 55 | "description": "zero integer matches schema", 56 | "data": 0, 57 | "valid": true 58 | }, 59 | { 60 | "description": "max value integer matches schema", 61 | "data": 42, 62 | "valid": true 63 | }, 64 | { 65 | "description": "max value float matches schema", 66 | "data": 42.0, 67 | "valid": true 68 | }, 69 | { 70 | "description": "over-value number does not match schema", 71 | "data": 43, 72 | "valid": false 73 | }, 74 | { 75 | "description": "large number does not match schema", 76 | "data": 999, 77 | "valid": false 78 | } 79 | ] 80 | }, 81 | 82 | { 83 | "description": "maximum with exclusiveMaximum", 84 | "schema": { 85 | "type": "number", 86 | "maximum": 42, 87 | "exclusiveMaximum": true 88 | }, 89 | "tests": [ 90 | { 91 | "description": "negative integer matches schema", 92 | "data": -100, 93 | "valid": true 94 | }, 95 | { 96 | "description": "zero integer matches schema", 97 | "data": 0, 98 | "valid": true 99 | }, 100 | { 101 | "description": "integer matches schema", 102 | "data": 41, 103 | "valid": true 104 | }, 105 | { 106 | "description": "float matches schema", 107 | "data": 41.0, 108 | "valid": true 109 | }, 110 | { 111 | "description": "max value integer does not match schema", 112 | "data": 42, 113 | "valid": false 114 | }, 115 | { 116 | "description": "max value float does not match schema", 117 | "data": 42.0, 118 | "valid": false 119 | }, 120 | { 121 | "description": "over-value number does not match schema", 122 | "data": 43, 123 | "valid": false 124 | } 125 | ] 126 | }, 127 | 128 | { 129 | "description": "maximum with exclusiveMaximum set to false should behave correctly", 130 | "schema": { 131 | "type": "number", 132 | "maximum": 42, 133 | "exclusiveMaximum": false 134 | }, 135 | "tests": [ 136 | { 137 | "description": "max value integer matches schema", 138 | "data": 42, 139 | "valid": true 140 | }, 141 | { 142 | "description": "max value float matches schema", 143 | "data": 42.0, 144 | "valid": true 145 | }, 146 | { 147 | "description": "over-value number does not match schema", 148 | "data": 43, 149 | "valid": false 150 | } 151 | ] 152 | }, 153 | 154 | { 155 | "description": "minimum", 156 | "schema": { 157 | "type": "number", 158 | "minimum": 42 159 | }, 160 | "tests": [ 161 | { 162 | "description": "min value integer matches schema", 163 | "data": 42, 164 | "valid": true 165 | }, 166 | { 167 | "description": "min value float matches schema", 168 | "data": 42.0, 169 | "valid": true 170 | }, 171 | { 172 | "description": "large number matches schema", 173 | "data": 999, 174 | "valid": true 175 | }, 176 | { 177 | "description": "zero does not match schema", 178 | "data": 0, 179 | "valid": false 180 | }, 181 | { 182 | "description": "negative number does not match schema", 183 | "data": -999, 184 | "valid": false 185 | } 186 | ] 187 | }, 188 | 189 | { 190 | "description": "minimum with exclusiveMinimum", 191 | "schema": { 192 | "type": "number", 193 | "minimum": 42, 194 | "exclusiveMinimum": true 195 | }, 196 | "tests": [ 197 | { 198 | "description": "min value integer does not match schema", 199 | "data": 42, 200 | "valid": false 201 | }, 202 | { 203 | "description": "min value float does not match schema", 204 | "data": 42.0, 205 | "valid": false 206 | }, 207 | { 208 | "description": "large number matches schema", 209 | "data": 999, 210 | "valid": true 211 | }, 212 | { 213 | "description": "zero does not match schema", 214 | "data": 0, 215 | "valid": false 216 | }, 217 | { 218 | "description": "negative number does not match schema", 219 | "data": -999, 220 | "valid": false 221 | } 222 | ] 223 | }, 224 | 225 | { 226 | "description": "minimum with exclusiveMinimum set to false should behave correctly", 227 | "schema": { 228 | "type": "number", 229 | "minimum": 42, 230 | "exclusiveMinimum": false 231 | }, 232 | "tests": [ 233 | { 234 | "description": "min value integer matches schema", 235 | "data": 42, 236 | "valid": true 237 | }, 238 | { 239 | "description": "min value float matches schema", 240 | "data": 42.0, 241 | "valid": true 242 | }, 243 | { 244 | "description": "under-value number does not match schema", 245 | "data": -999, 246 | "valid": false 247 | } 248 | ] 249 | } 250 | 251 | 252 | ] 253 | -------------------------------------------------------------------------------- /tests/our-tests/objects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "maxProperties", 4 | "schema": { 5 | "type": "object", 6 | "maxProperties": 3 7 | }, 8 | "tests": [ 9 | { 10 | "description": "matches schema", 11 | "data": { "foo": 1, "bar": 2, "baz": 3 }, 12 | "valid": true 13 | }, 14 | { 15 | "description": "too many properties does not match schema", 16 | "data": { "foo": 1, "bar": 2, "baz": 3, "qux": 4 }, 17 | "valid": false 18 | } 19 | ] 20 | }, 21 | 22 | { 23 | "description": "minProperties", 24 | "schema": { 25 | "type": "object", 26 | "minProperties": 3 27 | }, 28 | "tests": [ 29 | { 30 | "description": "matches schema", 31 | "data": { "foo": 1, "bar": 2, "baz": 3 }, 32 | "valid": true 33 | }, 34 | { 35 | "description": "too few properties does not match schema", 36 | "data": { "foo": 1, "bar": 2 }, 37 | "valid": false 38 | } 39 | ] 40 | }, 41 | 42 | { 43 | "description": "required", 44 | "schema": { 45 | "type": "object", 46 | "required": ["foo", "bar"] 47 | }, 48 | "tests": [ 49 | { 50 | "description": "matches schema", 51 | "data": { "foo": 1, "bar": 2 }, 52 | "valid": true 53 | }, 54 | { 55 | "description": "matches schema even with extra properties", 56 | "data": { "foo": 1, "bar": 2, "baz": 3 }, 57 | "valid": true 58 | }, 59 | { 60 | "description": "missing required property does not match schema", 61 | "data": { "foo": 1 }, 62 | "valid": false 63 | }, 64 | { 65 | "description": "empty object does not match schema", 66 | "data": {}, 67 | "valid": false 68 | } 69 | ] 70 | }, 71 | 72 | { 73 | "description": "additionalProperties true", 74 | "schema": { 75 | "type": "object", 76 | "additionalProperties": true 77 | }, 78 | "tests": [ 79 | { 80 | "description": "matches schema", 81 | "data": { "foo": 1, "bar": 2, "baz": 3 }, 82 | "valid": true 83 | } 84 | ] 85 | }, 86 | 87 | { 88 | "description": "additionalProperties is a schema", 89 | "schema": { 90 | "type": "object", 91 | "additionalProperties": {} 92 | }, 93 | "tests": [ 94 | { 95 | "description": "matches schema", 96 | "data": { "foo": 1, "bar": 2, "baz": 3 }, 97 | "valid": true 98 | } 99 | ] 100 | }, 101 | 102 | { 103 | "description": "additionalProperties is false", 104 | "schema": { 105 | "type": "object", 106 | "properties": { "foo": {}, "bar": {}, "baz": {} }, 107 | "additionalProperties": false 108 | }, 109 | "tests": [ 110 | { 111 | "description": "matches schema", 112 | "data": { "foo": 1, "bar": 2, "baz": 3 }, 113 | "valid": true 114 | }, 115 | { 116 | "description": "fewer properties than allowed still matches schema", 117 | "data": { "foo": 1, "bar": 2 }, 118 | "valid": true 119 | }, 120 | { 121 | "description": "does not match schema", 122 | "data": { "foo": 1, "bar": 2, "baz": 3, "qux": 4 }, 123 | "valid": false 124 | } 125 | ] 126 | }, 127 | 128 | { 129 | "description": "additionalProperties is false but patternProperties is specified", 130 | "schema": { 131 | "type": "object", 132 | "properties": { "foo": {} }, 133 | "patternProperties": { "^b": {} }, 134 | "additionalProperties": false 135 | }, 136 | "tests": [ 137 | { 138 | "description": "matches schema", 139 | "data": { "foo": 1, "bar": 2, "baz": 3 }, 140 | "valid": true 141 | }, 142 | { 143 | "description": "does not match schema", 144 | "data": { "foo": 1, "bar": 2, "qux": 4 }, 145 | "valid": false 146 | } 147 | ] 148 | }, 149 | 150 | { 151 | "description": "property dependencies", 152 | "schema": { 153 | "type": "object", 154 | "dependencies": { 155 | "foo": ["bar", "baz"] 156 | } 157 | }, 158 | "tests": [ 159 | { 160 | "description": "empty object matches schema", 161 | "data": {}, 162 | "valid": true 163 | }, 164 | { 165 | "description": "matches schema", 166 | "data": {"foo": 1, "bar": 2, "baz": 3}, 167 | "valid": true 168 | }, 169 | { 170 | "description": "does not match schema because of missing dependency", 171 | "data": { "foo": 1, "bar": 2, "qux": 4 }, 172 | "valid": false 173 | } 174 | ] 175 | }, 176 | 177 | { 178 | "description": "schema dependencies", 179 | "schema": { 180 | "type": "object", 181 | "dependencies": { 182 | "foo": { 183 | "required": ["bar", "baz"], 184 | "maxProperties": 3 185 | } 186 | } 187 | }, 188 | "tests": [ 189 | { 190 | "description": "empty object matches schema", 191 | "data": {}, 192 | "valid": true 193 | }, 194 | { 195 | "description": "matches schema", 196 | "data": {"foo": 1, "bar": 2, "baz": 3}, 197 | "valid": true 198 | }, 199 | { 200 | "description": "matches schema; maxProperties only enforced if foo present", 201 | "data": {"moo": 1, "bar": 2, "baz": 3, "qux": 4}, 202 | "valid": true 203 | }, 204 | { 205 | "description": "does not match schema because of missing dependency-required field", 206 | "data": {"foo": 1, "baz": 3}, 207 | "valid": false 208 | }, 209 | { 210 | "description": "does not match schema; only 3 props allowed if foo present", 211 | "data": {"foo": 1, "bar": 2, "baz": 3, "qux": 4}, 212 | "valid": false 213 | } 214 | ] 215 | }, 216 | 217 | { 218 | "description": "properties", 219 | "schema": { 220 | "type": "object", 221 | "properties": { 222 | "firstName": { "type": "string" }, 223 | "lastName": { "type": "string" }, 224 | "age": { "type": "integer" }, 225 | "payment": { "enum": [ "cash", "credit", null ] } 226 | } 227 | }, 228 | "tests": [ 229 | { 230 | "description": "matches schema", 231 | "data": { 232 | "firstName": "Mohammed", 233 | "lastName": "Chang", 234 | "age": 99, 235 | "city": "Lagos", 236 | "payment": "cash" 237 | }, 238 | "valid": true 239 | }, 240 | { 241 | "description": "matches schema", 242 | "data": { 243 | "firstName": "Mohammed", 244 | "lastName": "Chang", 245 | "age": 99, 246 | "city": "Lagos", 247 | "payment": null 248 | }, 249 | "valid": true 250 | }, 251 | { 252 | "description": "does not match schema", 253 | "data": { 254 | "firstName": "Mohammed", 255 | "lastName": "Chang", 256 | "age": 49.5, 257 | "city": "Lagos", 258 | "payment": "credit" 259 | }, 260 | "valid": false 261 | }, 262 | { 263 | "description": "does not match schema", 264 | "data": { 265 | "firstName": "Mohammed", 266 | "lastName": "Chang", 267 | "age": 23, 268 | "city": "Lagos", 269 | "payment": "wire" 270 | }, 271 | "valid": false 272 | } 273 | ] 274 | }, 275 | 276 | { 277 | "description": "patternProperties", 278 | "schema": { 279 | "type": "object", 280 | "properties": { 281 | "firstName": { "type": "string" }, 282 | "lastName": { "type": "string" }, 283 | "age": { "type": "integer" } 284 | }, 285 | "patternProperties": { 286 | "^address\\d+$": { "type": "string" } 287 | }, 288 | "additionalProperties": false 289 | }, 290 | "tests": [ 291 | { 292 | "description": "matches schema", 293 | "data": { 294 | "firstName": "Mohammed", 295 | "lastName": "Chang", 296 | "age": 99, 297 | "address1": "123 Main Street", 298 | "address2": "Suite A" 299 | }, 300 | "valid": true 301 | }, 302 | { 303 | "description": "does not match schema", 304 | "data": { 305 | "firstName": "Mohammed", 306 | "lastName": "Chang", 307 | "age": 99, 308 | "address1": "123 Main Street", 309 | "address2": "Suite A", 310 | "addressA": "Mailbox 7" 311 | }, 312 | "valid": false 313 | } 314 | ] 315 | } 316 | 317 | ] 318 | -------------------------------------------------------------------------------- /tests/our-tests/refs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "inline dereferencing", 4 | "schema": { 5 | "id": "http://foo.bar/baz#", 6 | "type": "number", 7 | "allOf": [{ "$ref": "#qux" }], 8 | "definitions": { 9 | "woo": { 10 | "id": "#qux", 11 | "maximum": 42 12 | } 13 | } 14 | }, 15 | "tests": [ 16 | { 17 | "description": "matches schema", 18 | "data": 41, 19 | "valid": true 20 | }, 21 | { 22 | "description": "does not match schema", 23 | "data": 43, 24 | "valid": false 25 | } 26 | ] 27 | }, 28 | 29 | { 30 | "description": "inline dereferencing of a JSON pointer", 31 | "schema": { 32 | "id": "http://foo.bar/schema1#", 33 | "type": "object", 34 | "properties": { 35 | "name": { "$ref": "#/definitions/name" } 36 | }, 37 | "definitions": { 38 | "name": { 39 | "type": "object", 40 | "required": ["first", "last"], 41 | "properties": { 42 | "first": { "type": "string" }, 43 | "last": { "type": "string" } 44 | } 45 | } 46 | } 47 | }, 48 | "tests": [ 49 | { 50 | "description": "matches schema", 51 | "data": { 52 | "name": { 53 | "first": "Mohammed", 54 | "last": "Chang" 55 | } 56 | }, 57 | "valid": true 58 | }, 59 | { 60 | "description": "does not match schema", 61 | "data": { 62 | "name": { 63 | "last": "Chang" 64 | } 65 | }, 66 | "valid": false 67 | } 68 | ] 69 | }, 70 | 71 | { 72 | "description": "inline dereferencing of a JSON pointer in an anonymous schema", 73 | "schema": { 74 | "type": "object", 75 | "properties": { 76 | "name": { "$ref": "#/definitions/name" } 77 | }, 78 | "definitions": { 79 | "name": { 80 | "type": "object", 81 | "required": ["first", "last"], 82 | "properties": { 83 | "first": { "type": "string" }, 84 | "last": { "type": "string" } 85 | } 86 | } 87 | } 88 | }, 89 | "tests": [ 90 | { 91 | "description": "matches schema", 92 | "data": { 93 | "name": { 94 | "first": "Mohammed", 95 | "last": "Chang" 96 | } 97 | }, 98 | "valid": true 99 | }, 100 | { 101 | "description": "does not match schema", 102 | "data": { 103 | "name": { 104 | "last": "Chang" 105 | } 106 | }, 107 | "valid": false 108 | } 109 | ] 110 | }, 111 | 112 | { 113 | "description": "use the correct resolution scope", 114 | "schema": { 115 | "type": "object", 116 | "id": "http://foo.bar/baz#", 117 | "properties": { 118 | "people": { 119 | "type": "array", 120 | "items": { "$ref": "#/definitions/name" } 121 | } 122 | }, 123 | 124 | "definitions": { 125 | "name": { 126 | "type": "object", 127 | "required": ["first", "last"], 128 | "properties": { 129 | "first": { "type": "string" }, 130 | "last": { "type": "string" } 131 | } 132 | } 133 | } 134 | }, 135 | "tests": [ 136 | { 137 | "description": "matches schema", 138 | "data": { 139 | "people": [ { "first": "Mohammed", "last": "Chang" } ] 140 | }, 141 | "valid": true 142 | }, 143 | { 144 | "description": "does not match schema", 145 | "data": { 146 | "people": [ { "last": "Chang" } ] 147 | }, 148 | "valid": false 149 | } 150 | ] 151 | }, 152 | 153 | { 154 | "description": "handle urn schema ids", 155 | "schema": { 156 | "type": "object", 157 | "id": "urn:foo:bar", 158 | "properties": { 159 | "people": { 160 | "type": "array", 161 | "items": { "$ref": "#/definitions/name" } 162 | } 163 | }, 164 | 165 | "definitions": { 166 | "name": { 167 | "type": "object", 168 | "required": ["first", "last"], 169 | "properties": { 170 | "first": { "type": "string" }, 171 | "last": { "type": "string" } 172 | } 173 | } 174 | } 175 | }, 176 | "tests": [ 177 | { 178 | "description": "matches schema", 179 | "data": { 180 | "people": [ { "first": "Mohammed", "last": "Chang" } ] 181 | }, 182 | "valid": true 183 | }, 184 | { 185 | "description": "does not match schema", 186 | "data": { 187 | "people": [ { "last": "Chang" } ] 188 | }, 189 | "valid": false 190 | } 191 | ] 192 | }, 193 | { 194 | "description": "deep nested refs", 195 | "schema": { 196 | "definitions": { 197 | "a": {"type": "integer"}, 198 | "b": {"$ref": "#/definitions/a"}, 199 | "c": {"$ref": "#/definitions/b"}, 200 | "d": {"$ref": "#/definitions/c"}, 201 | "e": {"$ref": "#/definitions/d"}, 202 | "f": {"$ref": "#/definitions/e"}, 203 | "g": {"$ref": "#/definitions/f"}, 204 | "h": {"$ref": "#/definitions/g"}, 205 | "i": {"$ref": "#/definitions/h"} 206 | }, 207 | "$ref": "#/definitions/i" 208 | }, 209 | "tests": [ 210 | { 211 | "description": "nested ref valid", 212 | "data": 5, 213 | "valid": true 214 | }, 215 | { 216 | "description": "nested ref invalid", 217 | "data": "a", 218 | "valid": false 219 | } 220 | ] 221 | }, 222 | { 223 | "description": "deep nested refs (too deep)", 224 | "schema": { 225 | "definitions": { 226 | "a": {"type": "integer"}, 227 | "b": {"$ref": "#/definitions/a"}, 228 | "c": {"$ref": "#/definitions/b"}, 229 | "d": {"$ref": "#/definitions/c"}, 230 | "e": {"$ref": "#/definitions/d"}, 231 | "f": {"$ref": "#/definitions/e"}, 232 | "g": {"$ref": "#/definitions/f"}, 233 | "h": {"$ref": "#/definitions/g"}, 234 | "i": {"$ref": "#/definitions/h"}, 235 | "j": {"$ref": "#/definitions/i"} 236 | }, 237 | "$ref": "#/definitions/j" 238 | }, 239 | "tests": [ 240 | { 241 | "description": "nested ref invalid", 242 | "data": "a", 243 | "valid": false 244 | } 245 | ] 246 | }, 247 | { 248 | "description": "circular refs should not cause infinite loop", 249 | "schema": { 250 | "definitions": { 251 | "a": {"$ref": "#/definitions/b"}, 252 | "b": {"$ref": "#/definitions/a"} 253 | }, 254 | "$ref": "#/definitions/b" 255 | }, 256 | "tests": [ 257 | { 258 | "description": "nested ref invalid", 259 | "data": "a", 260 | "valid": false 261 | } 262 | ] 263 | } 264 | ] 265 | -------------------------------------------------------------------------------- /tests/our-tests/refsAsync.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "asynchronously resolve a reference to an external schema", 4 | "schema": { 5 | "properties": { 6 | "location": { "$ref": "http://www.jayschema.org/test-targets/geo.json#" } 7 | } 8 | }, 9 | "tests": [ 10 | { 11 | "description": "matches schema", 12 | "data": { 13 | "location": { 14 | "latitude": 48.8583, 15 | "longitude": 2.2945 16 | } 17 | }, 18 | "valid": true 19 | }, 20 | { 21 | "description": "does not match schema", 22 | "data": { 23 | "location": { 24 | "latitude": "hello" 25 | } 26 | }, 27 | "valid": false 28 | } 29 | ] 30 | }, 31 | 32 | { 33 | "description": "asynchronously resolve a reference to an external schema (bare URI)", 34 | "schema": { 35 | "properties": { 36 | "location": { "$ref": "http://www.jayschema.org/test-targets/geo.json" } 37 | } 38 | }, 39 | "tests": [ 40 | { 41 | "description": "matches schema", 42 | "data": { 43 | "location": { 44 | "latitude": 48.8583, 45 | "longitude": 2.2945 46 | } 47 | }, 48 | "valid": true 49 | }, 50 | { 51 | "description": "does not match schema", 52 | "data": { 53 | "location": { 54 | "latitude": "hello" 55 | } 56 | }, 57 | "valid": false 58 | } 59 | ] 60 | } 61 | 62 | ] 63 | -------------------------------------------------------------------------------- /tests/our-tests/refsSync.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "references to external schemas aren't resolved unless using async", 4 | "schema": { 5 | "properties": { 6 | "location": { "$ref": "http://www.jayschema.org/test-targets/geo.json#" } 7 | } 8 | }, 9 | "tests": [ 10 | { 11 | "description": "fails because referenced schema can't be retrieved in sync mode", 12 | "data": { 13 | "location": { 14 | "latitude": 48.8583, 15 | "longitude": 2.2945 16 | } 17 | }, 18 | "valid": false 19 | } 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /tests/our-tests/strings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "maxLength", 4 | "schema": { 5 | "type": "string", 6 | "maxLength": 10 7 | }, 8 | "tests": [ 9 | { 10 | "description": "empty string matches schema", 11 | "data": "", 12 | "valid": true 13 | }, 14 | { 15 | "description": "short string matches schema", 16 | "data": "hello", 17 | "valid": true 18 | }, 19 | { 20 | "description": "max length string matches schema", 21 | "data": "0123456789", 22 | "valid": true 23 | }, 24 | { 25 | "description": "over-length string does not match schema", 26 | "data": "hello, world!", 27 | "valid": false 28 | } 29 | 30 | ] 31 | }, 32 | 33 | { 34 | "description": "minLength", 35 | "schema": { 36 | "type": "string", 37 | "minLength": 10 38 | }, 39 | "tests": [ 40 | { 41 | "description": "long string matches schema", 42 | "data": "hello, world!", 43 | "valid": true 44 | }, 45 | { 46 | "description": "min-length string matches schema", 47 | "data": "0123456789", 48 | "valid": true 49 | }, 50 | { 51 | "description": "empty string does not match schema", 52 | "data": "", 53 | "valid": false 54 | }, 55 | { 56 | "description": "short string does not match schema", 57 | "data": "hello", 58 | "valid": false 59 | } 60 | ] 61 | }, 62 | 63 | { 64 | "description": "pattern (non-anchored regex)", 65 | "schema": { 66 | "type": "string", 67 | "pattern": "hello" 68 | }, 69 | "tests": [ 70 | { 71 | "description": "string matches schema", 72 | "data": "I said hello to her.", 73 | "valid": true 74 | }, 75 | { 76 | "description": "string does not match schema", 77 | "data": "Hello, World", 78 | "valid": false 79 | } 80 | ] 81 | }, 82 | 83 | { 84 | "description": "pattern (anchored regex)", 85 | "schema": { 86 | "type": "string", 87 | "pattern": "^hello" 88 | }, 89 | "tests": [ 90 | { 91 | "description": "string matches schema", 92 | "data": "hello, world", 93 | "valid": true 94 | }, 95 | { 96 | "description": "string does not match schema", 97 | "data": "I said hello to her.", 98 | "valid": false 99 | } 100 | ] 101 | }, 102 | 103 | { 104 | "description": "pattern (comples regex)", 105 | "schema": { 106 | "type": "string", 107 | "pattern": "^You owe \\$\\d+(\\.\\d+)?\\.$" 108 | }, 109 | "tests": [ 110 | { 111 | "description": "string matches schema", 112 | "data": "You owe $17.69.", 113 | "valid": true 114 | }, 115 | { 116 | "description": "string does not match schema", 117 | "data": "You owe $0.nothing.", 118 | "valid": false 119 | } 120 | ] 121 | } 122 | 123 | 124 | ] 125 | -------------------------------------------------------------------------------- /tests/our-tests/testThatApplyToAllTypes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "enum keyword", 4 | "schema": { 5 | "enum": ["foo", "bar"] 6 | }, 7 | "tests": [ 8 | { 9 | "description": "item matches schema", 10 | "data": "foo", 11 | "valid": true 12 | }, 13 | { 14 | "description": "item does not match schema", 15 | "data": "baz", 16 | "valid": false 17 | }, 18 | { 19 | "description": "integer does not match schema", 20 | "data": 42, 21 | "valid": false 22 | } 23 | ] 24 | }, 25 | 26 | { 27 | "description": "enum keyword with mixed types", 28 | "schema": { 29 | "enum": ["foo", "bar", 42] 30 | }, 31 | "tests": [ 32 | { 33 | "description": "item matches schema", 34 | "data": "foo", 35 | "valid": true 36 | }, 37 | { 38 | "description": "item does not match schema", 39 | "data": "baz", 40 | "valid": false 41 | }, 42 | { 43 | "description": "integer matches schema", 44 | "data": 42, 45 | "valid": true 46 | }, 47 | { 48 | "description": "string similar to integer does not match schema", 49 | "data": "42", 50 | "valid": false 51 | }, 52 | { 53 | "description": "empty string does not match schema", 54 | "data": "", 55 | "valid": false 56 | }, 57 | { 58 | "description": "null does not match schema", 59 | "data": null, 60 | "valid": false 61 | }, 62 | { 63 | "description": "array does not match schema", 64 | "data": [], 65 | "valid": false 66 | }, 67 | { 68 | "description": "object does not match schema", 69 | "data": {}, 70 | "valid": false 71 | } 72 | ] 73 | }, 74 | 75 | { 76 | "description": "enum keyword with null accepted", 77 | "schema": { 78 | "enum": ["foo", "bar", null] 79 | }, 80 | "tests": [ 81 | { 82 | "description": "item matches schema", 83 | "data": "foo", 84 | "valid": true 85 | }, 86 | { 87 | "description": "item matches schema", 88 | "data": "bar", 89 | "valid": true 90 | }, 91 | { 92 | "description": "item matches schema", 93 | "data": null, 94 | "valid": true 95 | }, 96 | { 97 | "description": "item does not match schema", 98 | "data": 42, 99 | "valid": false 100 | } 101 | ] 102 | }, 103 | 104 | { 105 | "description": "allOf keyword", 106 | "schema": { 107 | "type": "array", 108 | "allOf": [ 109 | { "items": { "type": "number" } }, 110 | { "maxItems": 3 } 111 | ] 112 | }, 113 | "tests": [ 114 | { 115 | "description": "empty array matches schema", 116 | "data": [], 117 | "valid": true 118 | }, 119 | { 120 | "description": "array of numbers matches schema", 121 | "data": [1, 2, 3], 122 | "valid": true 123 | }, 124 | { 125 | "description": "array violates first of two 'allOf' schemas", 126 | "data": [1, "2", 3], 127 | "valid": false 128 | }, 129 | { 130 | "description": "array violates second of two 'allOf' schemas", 131 | "data": [1, 2, 3, 4], 132 | "valid": false 133 | }, 134 | { 135 | "description": "array violates both of the 'allOf' schemas", 136 | "data": [1, 2, "3", 4], 137 | "valid": false 138 | } 139 | ] 140 | }, 141 | 142 | { 143 | "description": "anyOf keyword", 144 | "schema": { 145 | "type": "array", 146 | "anyOf": [ 147 | { "items": { "type": "number" } }, 148 | { "maxItems": 3 } 149 | ] 150 | }, 151 | "tests": [ 152 | { 153 | "description": "empty array matches schema", 154 | "data": [], 155 | "valid": true 156 | }, 157 | { 158 | "description": "array of numbers matches schema", 159 | "data": [1, 2, 3], 160 | "valid": true 161 | }, 162 | { 163 | "description": "matches second of the two 'anyOf' schemas", 164 | "data": [1, "2", 3], 165 | "valid": true 166 | }, 167 | { 168 | "description": "matches first of the two 'anyOf' schemas", 169 | "data": [1, 2, 3, 4], 170 | "valid": true 171 | }, 172 | { 173 | "description": "array violates both of the 'anyOf' schemas", 174 | "data": [1, 2, "3", 4], 175 | "valid": false 176 | } 177 | ] 178 | }, 179 | 180 | { 181 | "description": "oneOf keyword", 182 | "schema": { 183 | "type": "array", 184 | "oneOf": [ 185 | { "items": { "type": "number" } }, 186 | { "maxItems": 3 } 187 | ] 188 | }, 189 | "tests": [ 190 | { 191 | "description": "mixed array matches schema", 192 | "data": [1, "2", 3], 193 | "valid": true 194 | }, 195 | { 196 | "description": "empty array does not match schema", 197 | "data": [], 198 | "valid": false 199 | }, 200 | { 201 | "description": "array matches both 'oneOf' schemas, so fails to match overall", 202 | "data": [1, 2, 3], 203 | "valid": false 204 | }, 205 | { 206 | "description": "array matches neither of the 'oneOf' schemas", 207 | "data": [1, 2, 3, "4"], 208 | "valid": false 209 | } 210 | ] 211 | }, 212 | 213 | { 214 | "description": "not keyword", 215 | "schema": { 216 | "type": "number", 217 | "not": { "multipleOf": 8 } 218 | }, 219 | "tests": [ 220 | { 221 | "description": "does not match 'not' schema, so DOES match overall schema", 222 | "data": 63, 223 | "valid": true 224 | }, 225 | { 226 | "description": "matches 'not' schema, so DOES NOT match overall schema", 227 | "data": 64, 228 | "valid": false 229 | } 230 | ] 231 | } 232 | ] 233 | -------------------------------------------------------------------------------- /tests/our-tests/topLevelSchemas.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "current-version $schema spec is allowed", 4 | "schema": { 5 | "$schema": "http://json-schema.org/schema#", 6 | "type": "integer" 7 | }, 8 | "tests": [ 9 | { 10 | "description": "matches schema", 11 | "data": 42, 12 | "valid": true 13 | }, 14 | { 15 | "description": "does not match schema", 16 | "data": "abc", 17 | "valid": false 18 | } 19 | ] 20 | }, 21 | 22 | { 23 | "description": "explicit draft 4 $schema spec is allowed", 24 | "schema": { 25 | "$schema": "http://json-schema.org/draft-04/schema#", 26 | "type": "integer" 27 | }, 28 | "tests": [ 29 | { 30 | "description": "matches schema", 31 | "data": 42, 32 | "valid": true 33 | }, 34 | { 35 | "description": "does not match schema", 36 | "data": "abc", 37 | "valid": false 38 | } 39 | ] 40 | }, 41 | 42 | { 43 | "description": "unrecognized $schema spec is not allowed", 44 | "schema": { 45 | "$schema": "http://json-schema.org/draft-99/schema#", 46 | "type": "integer" 47 | }, 48 | "tests": [ 49 | { 50 | "description": "fails against unrecognized $schema", 51 | "data": 42, 52 | "valid": false 53 | } 54 | ] 55 | } 56 | ] -------------------------------------------------------------------------------- /tests/refs.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true should:true */ 4 | 5 | 6 | var should = require('should') 7 | , JaySchema = require('../lib/jayschema.js') 8 | ; 9 | 10 | describe('JSON references:', 11 | function() 12 | { 13 | describe('reference previously manually-registered schema:', function() { 14 | 15 | var jj = new JaySchema(); 16 | var sch; 17 | 18 | var otherSchema = { 19 | id: 'http://foo.bar/name#', 20 | type: 'object', 21 | required: ['first', 'last'], 22 | properties: { 23 | first: { $ref: '#/definitions/nameField' }, 24 | last: { type: 'string' } 25 | }, 26 | definitions: { 27 | nameField: { type: 'string' } 28 | } 29 | }; 30 | 31 | jj.register(otherSchema); 32 | 33 | it('should validate', function() { 34 | sch = { 35 | type: 'object', 36 | properties: { 37 | name: { $ref: 'http://foo.bar/name#' } 38 | } 39 | }; 40 | 41 | jj.validate({name: {first: 'Mohammed', last: 'Chang'}}, sch) 42 | .should.be.empty; 43 | }); 44 | 45 | it('should fail validation', function() { 46 | sch = { 47 | type: 'object', 48 | properties: { 49 | name: { $ref: 'http://foo.bar/name#' } 50 | } 51 | }; 52 | 53 | jj.validate({name: {last: 'Chang'}}, sch).should.not.be.empty; 54 | }); 55 | 56 | }); 57 | 58 | describe('validate using the string id of a registered schema', function() { 59 | 60 | var jj = new JaySchema(); 61 | 62 | var schema = { 63 | id: 'http://foo.bar/name#', 64 | type: 'object', 65 | required: ['first', 'last'], 66 | properties: { 67 | first: { $ref: '#/definitions/nameField' }, 68 | last: { type: 'string' } 69 | }, 70 | definitions: { 71 | nameField: { type: 'string' } 72 | } 73 | }; 74 | 75 | jj.register(schema); 76 | 77 | it('should validate', function() { 78 | var data = { 79 | 'first': 'John', 80 | 'middle': 'Q.', 81 | 'last': 'Public' 82 | }; 83 | 84 | jj.validate(data, 'http://foo.bar/name#').should.be.empty; 85 | }); 86 | 87 | it('should fail validation', function() { 88 | var data = { 89 | 'first': 'John' 90 | }; 91 | 92 | jj.validate(data, 'http://foo.bar/name#').should.not.be.empty; 93 | }); 94 | 95 | }); 96 | 97 | describe('ensure that JaySchema.prototype.isRegistered(id) works', 98 | function() 99 | { 100 | var jj = new JaySchema(); 101 | var sch = { 102 | id: 'http://foo.bar/baz#', 103 | type: 'string', 104 | definitions: { 105 | qux: { id: '#qux', type: 'integer' } 106 | } 107 | }; 108 | jj.register(sch); 109 | 110 | it('should show the schema is registered', function() { 111 | jj.isRegistered('http://foo.bar/baz#').should.be.true; 112 | jj.isRegistered('http://foo.bar/baz').should.be.true; 113 | jj.isRegistered('http://foo.bar/baz#qux').should.be.true; 114 | }); 115 | 116 | it('should show the schema is not registered', function() { 117 | jj.isRegistered('http://qux.zzz/baz#').should.be.false; 118 | }); 119 | 120 | }); 121 | 122 | describe('ensure async validation works even if no external schemas are ' + 123 | 'referenced', function() 124 | { 125 | 126 | var instance = 63; 127 | var schema = { 'type': 'integer', 'multipleOf': 8 }; 128 | 129 | var loader = function (ref, callback) { 130 | process.nextTick(function () { 131 | callback(null, schema); 132 | }); 133 | }; 134 | 135 | var jj = new JaySchema(loader); 136 | 137 | it('should not time out', function(done) { 138 | jj.validate(instance, schema, function(errs) { 139 | should.exist(errs); 140 | done(); 141 | }); 142 | }); 143 | }); 144 | 145 | }); 146 | -------------------------------------------------------------------------------- /tests/schemaRegistry.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true */ 4 | 5 | 6 | var should = require('should') 7 | , SchemaRegistry = require('../lib/schemaRegistry.js') 8 | , core = require('../lib/suites/draft-04/core.js') 9 | , uri = require('../lib/uri.js') 10 | ; 11 | 12 | describe('SchemaRegistry:', function() { 13 | 14 | describe('register basic schema:', function() { 15 | 16 | var reg = new SchemaRegistry(); 17 | var sch = { 18 | id: 'http://foo.bar/baz', 19 | type: 'integer' 20 | }; 21 | 22 | it('should register schema', function() { 23 | reg.register(sch).should.be.empty; 24 | var result = reg.get(sch.id); 25 | core.jsonEqual(result, sch).should.be.true; 26 | }); 27 | 28 | it('should retrieve schema differing only by empty fragment', function() { 29 | reg.register(sch).should.be.empty; 30 | var result = reg.get(sch.id + '#'); 31 | core.jsonEqual(result, sch).should.be.true; 32 | }); 33 | 34 | }); 35 | 36 | describe('register schema with external reference:', function() { 37 | 38 | var reg = new SchemaRegistry(); 39 | var sch = { 40 | id: 'http://foo.bar/baz', 41 | oneOf: [ 42 | { $ref: 'http://this.is.missing/qux#' } 43 | ] 44 | }; 45 | 46 | it('should register schema', function() { 47 | reg.register(sch).should.eql(['http://this.is.missing/qux']); 48 | var result = reg.get(sch.id); 49 | core.jsonEqual(result, sch).should.be.true; 50 | }); 51 | 52 | it('second register call for the same should return same missing list', 53 | function() 54 | { 55 | reg.register(sch).should.eql(['http://this.is.missing/qux']); 56 | reg.register(sch).should.eql(['http://this.is.missing/qux']); 57 | }); 58 | }); 59 | 60 | describe('schema with /definitions section:', function() { 61 | 62 | var reg = new SchemaRegistry(); 63 | var sch = { 64 | id: 'http://foo.bar/baz', 65 | oneOf: [ 66 | { $ref: '#/definitions/foo' } 67 | ], 68 | definitions: { 69 | foo: { type: 'integer' }, 70 | bar: { id: '#bar', type: 'string' } 71 | } 72 | }; 73 | 74 | it('should register schema and sub-schemas', function() { 75 | reg.register(sch).should.be.empty; 76 | reg.get(sch.id + '#/definitions/foo').should.eql({type: 'integer'}); 77 | reg.get(sch.id + '#bar').should.eql({id: '#bar', type: 'string'}); 78 | reg.get(sch.id + '#/definitions/bar').should.eql({id: '#bar', 79 | type: 'string'}); 80 | }); 81 | }); 82 | 83 | describe('multiple missing schemas:', function() { 84 | 85 | var reg = new SchemaRegistry(); 86 | var sch1 = { 87 | id: 'http://foo.bar/baz', 88 | oneOf: [ 89 | { $ref: 'http://company.com/foo/' } 90 | ], 91 | definitions: { 92 | foo: { type: 'integer' }, 93 | bar: { id: '#bar', type: 'string' }, 94 | qux: { $ref: 'http://organization.org/bar/' } 95 | } 96 | }; 97 | var sch2 = { 98 | oneOf: [ 99 | { $ref: 'http://organization.org/bar/' }, 100 | { $ref: 'http://foo.bar/qux' }, 101 | { $ref: 'http://some.site/and/some/schema#' } 102 | ] 103 | }; 104 | 105 | it('should be able to return merged missing schemas', function() { 106 | var missing; 107 | missing = reg.register(sch1); 108 | missing.should.have.length(2); 109 | missing.should.containEql('http://company.com/foo/'); 110 | missing.should.containEql('http://organization.org/bar/'); 111 | 112 | missing = reg.register(sch2); 113 | missing.should.have.length(3); 114 | missing.should.containEql('http://organization.org/bar/'); 115 | missing.should.containEql('http://foo.bar/qux'); 116 | missing.should.containEql('http://some.site/and/some/schema'); 117 | 118 | missing = reg.getMissingSchemas(); 119 | missing.should.have.length(4); 120 | missing.should.containEql('http://company.com/foo/'); 121 | missing.should.containEql('http://organization.org/bar/'); 122 | missing.should.containEql('http://foo.bar/qux'); 123 | missing.should.containEql('http://some.site/and/some/schema'); 124 | }); 125 | }); 126 | 127 | describe('resolve fragments:', function() { 128 | var schema = { 129 | "id": "http://x.y.z/rootschema.json#", 130 | "schema1": { 131 | "id": "#foo" 132 | }, 133 | "schema2": { 134 | "id": "otherschema.json", 135 | "nested": { 136 | "id": "#bar" 137 | }, 138 | "alsonested": { 139 | "id": "t/inner.json#a" 140 | } 141 | }, 142 | "schema3": { 143 | "id": "some://where.else/completely#" 144 | } 145 | } 146 | 147 | var reg = new SchemaRegistry(); 148 | reg.register(schema); 149 | 150 | var url = require('url'); 151 | 152 | it('should resolve fragment-identified schemas', function() 153 | { 154 | var scope, fragment, result; 155 | scope = 'http://x.y.z/rootschema.json#'; 156 | 157 | fragment = '#'; 158 | result = reg.get(uri.resolve(scope, fragment)); 159 | result.should.eql(schema); 160 | 161 | fragment = '#/schema1'; 162 | result = reg.get(uri.resolve(scope, fragment)); 163 | result.should.eql(schema.schema1); 164 | 165 | fragment = '#foo'; 166 | result = reg.get(uri.resolve(scope, fragment)); 167 | result.should.eql(schema.schema1); 168 | 169 | fragment = '#/schema2'; 170 | result = reg.get(uri.resolve(scope, fragment)); 171 | result.should.eql(schema.schema2); 172 | 173 | fragment = '#/schema2/nested'; 174 | result = reg.get(uri.resolve(scope, fragment)); 175 | result.should.eql(schema.schema2.nested); 176 | 177 | fragment = '#/schema2/alsonested'; 178 | result = reg.get(uri.resolve(scope, fragment)); 179 | result.should.eql(schema.schema2.alsonested); 180 | 181 | fragment = '#/schema3'; 182 | result = reg.get(uri.resolve(scope, fragment)); 183 | result.should.eql(schema.schema3); 184 | 185 | scope = 'http://x.y.z/otherschema.json'; 186 | fragment = '#'; 187 | result = reg.get(uri.resolve(scope, fragment)); 188 | result.should.eql(schema.schema2); 189 | 190 | fragment = '#bar'; 191 | result = reg.get(uri.resolve(scope, fragment)); 192 | result.should.eql(schema.schema2.nested); 193 | 194 | scope = 'http://x.y.z/t/inner.json'; 195 | fragment = '#a'; 196 | result = reg.get(uri.resolve(scope, fragment)); 197 | result.should.eql(schema.schema2.alsonested); 198 | }); 199 | }); 200 | 201 | describe('isRegistered:', function() { 202 | 203 | var reg = new SchemaRegistry(); 204 | var sch = { 205 | id: 'http://foo.bar/baz#', 206 | type: 'integer' 207 | }; 208 | reg.register(sch); 209 | 210 | it('should find the registered schema', function() { 211 | reg.isRegistered('http://foo.bar/baz#').should.be.true; 212 | }); 213 | 214 | it('should find the registered schema if the request is missing a "#"', 215 | function() 216 | { 217 | reg.isRegistered('http://foo.bar/baz').should.be.true; 218 | }); 219 | 220 | it('should not find an unregistered schema', function() { 221 | reg.isRegistered('http://foo.bar/qux').should.be.false; 222 | }); 223 | 224 | }); 225 | 226 | describe('test for bug exposed in issue #2:', function() { 227 | 228 | var addressSchema = { 229 | "id": "https://example.org/api/address.schema.json", 230 | "type": "string" 231 | }; 232 | 233 | var masterSchema = { 234 | "title": "user", 235 | "id": "https://example.org/api/user.schema.json", 236 | "properties": { 237 | "addresses": { 238 | "type": "array", 239 | "items": { 240 | "$ref": "https://example.org/api/address.schema.json" 241 | } 242 | } 243 | } 244 | }; 245 | 246 | var data = { 247 | "addresses": [ 248 | "address1", 249 | "address2", 250 | "address3" 251 | ] 252 | }; 253 | 254 | it('should show no missing schemas', function() { 255 | var reg = new SchemaRegistry(); 256 | reg.register(addressSchema).should.be.empty; 257 | reg.register(masterSchema).should.be.empty; 258 | }); 259 | 260 | it('should show a missing schema when registered in reverse order', 261 | function() 262 | { 263 | var reg = new SchemaRegistry(); 264 | 265 | var missing; 266 | missing = reg.register(masterSchema); 267 | missing.should.have.length(1); 268 | missing.should.containEql('https://example.org/api/address.schema.json'); 269 | 270 | missing = reg.register(addressSchema); 271 | missing.should.be.empty; 272 | }); 273 | 274 | }); 275 | }); 276 | -------------------------------------------------------------------------------- /tests/subSchemas.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true should:true */ 4 | 5 | 6 | var should = require('should') 7 | , JaySchema = require('../lib/jayschema.js') 8 | ; 9 | 10 | describe('Sub-schemas:', 11 | function() 12 | { 13 | // test for issue found in #42 14 | describe('anonymous sub-schemas used in failed oneOf validation should not ' + 15 | 'overwrite earlier error messages:', function() 16 | { 17 | var js = new JaySchema(); 18 | 19 | var schema = { 20 | type: 'object', 21 | properties: { 22 | result: { 23 | oneOf: [ 24 | { type: 'string' }, 25 | { type: 'number', 'enum': [400, 401, 404, 500] } 26 | ] 27 | } 28 | } 29 | }; 30 | 31 | var instance = { result: false }; 32 | var errs = js.validate(instance, schema); 33 | 34 | it('should show separate validation errors for each sub-schema', function() 35 | { 36 | errs.should.be.instanceOf(Array).and.have.lengthOf(1); 37 | errs[0].should.have.property('subSchemaValidationErrors'); 38 | var ssves = errs[0].subSchemaValidationErrors; 39 | ssves.should.have.property('sub-schema-1'); 40 | ssves.should.have.property('sub-schema-2'); 41 | }); 42 | }); 43 | 44 | describe('anonymous sub-schemas used in failed anyOf validation should not ' + 45 | 'overwrite earlier error messages:', function() 46 | { 47 | var js = new JaySchema(); 48 | 49 | var schema = { 50 | type: 'object', 51 | properties: { 52 | result: { 53 | anyOf: [ 54 | { type: 'string' }, 55 | { type: 'number', 'enum': [400, 401, 404, 500] } 56 | ] 57 | } 58 | } 59 | }; 60 | 61 | var instance = { result: false }; 62 | var errs = js.validate(instance, schema); 63 | 64 | it('should show separate validation errors for each sub-schema', function() 65 | { 66 | errs.should.be.instanceOf(Array).and.have.lengthOf(1); 67 | errs[0].should.have.property('subSchemaValidationErrors'); 68 | var ssves = errs[0].subSchemaValidationErrors; 69 | ssves.should.have.property('sub-schema-1'); 70 | ssves.should.have.property('sub-schema-2'); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/uri.js: -------------------------------------------------------------------------------- 1 | // Unit tests. Run with mocha. 2 | 3 | /*global describe:true it:true */ 4 | 5 | 6 | var should = require('should') 7 | , SchemaRegistry = require('../lib/schemaRegistry.js') 8 | , uri = require('../lib/uri.js') 9 | ; 10 | 11 | describe('URI module:', function() { 12 | 13 | describe('parse:', function() { 14 | 15 | it('should parse a URL', function() { 16 | var url = 'http://google.com/foo/bar/baz#qux'; 17 | var uriObj = uri.parse(url); 18 | uriObj.kind.should.eql('url'); 19 | uriObj.absolute.should.be.true; 20 | uriObj.baseUri.should.eql('http://google.com/foo/bar/baz'); 21 | uriObj.fragment.should.eql('#qux'); 22 | }); 23 | 24 | it('should parse a URN', function() { 25 | var urn = 'urn:uuid:FB8E5076-BF6F-4DED-8165-8369A9158B46#foo'; 26 | var uriObj = uri.parse(urn); 27 | uriObj.kind.should.eql('urn'); 28 | uriObj.baseUri.should.eql('urn:uuid:FB8E5076-BF6F-4DED-8165-8369A9158B46'); 29 | uriObj.fragment.should.eql('#foo'); 30 | }); 31 | 32 | it('should parse a URL with absolute path but no host', function() { 33 | var url = '/bar/baz#qux'; 34 | var uriObj = uri.parse(url); 35 | uriObj.kind.should.eql('url'); 36 | uriObj.absolute.should.be.false; 37 | uriObj.baseUri.should.eql('/bar/baz'); 38 | uriObj.fragment.should.eql('#qux'); 39 | }); 40 | 41 | it('should parse a relative URL', function() { 42 | var url = 'bar/baz#qux'; 43 | var uriObj = uri.parse(url); 44 | uriObj.kind.should.eql('url'); 45 | uriObj.absolute.should.be.false; 46 | uriObj.baseUri.should.eql('bar/baz'); 47 | uriObj.fragment.should.eql('#qux'); 48 | }); 49 | 50 | it('should round-trip format a URL', function() { 51 | var url = 'http://some.site/foo/bar/baz#qux'; 52 | var uriObj = uri.parse(url); 53 | uri.format(uriObj).should.eql(url); 54 | }); 55 | 56 | it('should round-trip format a URN', function() { 57 | var urn = 'urn:uuid:FB8E5076-BF6F-4DED-8165-8369A9158B46#foo'; 58 | var uriObj = uri.parse(urn); 59 | uri.format(uriObj).should.eql(urn); 60 | }); 61 | 62 | it('should add a hash sign to a URL that does not have one', function() { 63 | var url = 'http://some.site/foo/bar/baz'; 64 | var uriObj = uri.parse(url); 65 | uri.format(uriObj).slice(-1).should.eql('#'); 66 | }); 67 | 68 | it('should add a hash sign to a URN that does not have one', function() { 69 | var urn = 'urn:uuid:FB8E5076-BF6F-4DED-8165-8369A9158B46'; 70 | var uriObj = uri.parse(urn); 71 | uri.format(uriObj).slice(-1).should.eql('#'); 72 | }); 73 | 74 | it('should resolve in favor of an absolute URI', function() { 75 | var from = 'http://google.com/some/search'; 76 | var to = 'http://bing.com/another/search'; 77 | uri.resolve(from, to).should.eql(to + '#'); 78 | }); 79 | 80 | it('should resolve in favor of a URN', function() { 81 | var from = 'http://google.com/some/search'; 82 | var to = 'urn:uuid:FB8E5076-BF6F-4DED-8165-8369A9158B46'; 83 | uri.resolve(from, to).should.eql(to + '#'); 84 | }); 85 | 86 | it('should resolve a relative URL onto an absolute URL', function() { 87 | var from = 'http://google.com/some/search#foo'; 88 | var to = 'other/page#bar'; 89 | var expected = 'http://google.com/some/other/page#bar'; 90 | uri.resolve(from, to).should.eql(expected); 91 | }); 92 | 93 | it('should resolve an absolute pathname onto an absolute URL', function() { 94 | var from = 'http://google.com/some/search#foo'; 95 | var to = '/other/page#bar'; 96 | var expected = 'http://google.com/other/page#bar'; 97 | uri.resolve(from, to).should.eql(expected); 98 | }); 99 | 100 | it('should resolve a fragment onto an absolute URL', function() { 101 | var from = 'http://google.com/some/search#foo'; 102 | var to = '#bar'; 103 | var expected = 'http://google.com/some/search#bar'; 104 | uri.resolve(from, to).should.eql(expected); 105 | }); 106 | 107 | it('should resolve a relative URL onto a URN', function() { 108 | var from = 'urn:uuid:3AAB9A9E-65BD-4BDB-A514-318205A3E409/foo/bar#qux'; 109 | var to = 'other/page#bar'; 110 | var expected = 111 | 'urn:uuid:3AAB9A9E-65BD-4BDB-A514-318205A3E409/foo/other/page#bar'; 112 | uri.resolve(from, to).should.eql(expected); 113 | }); 114 | 115 | it('should resolve an absolute pathname onto a URN', function() { 116 | var from = 'urn:uuid:3AAB9A9E-65BD-4BDB-A514-318205A3E409/foo/bar#qux'; 117 | var to = '/other/page#bar'; 118 | var expected = 119 | 'urn:uuid:3AAB9A9E-65BD-4BDB-A514-318205A3E409/other/page#bar'; 120 | uri.resolve(from, to).should.eql(expected); 121 | }); 122 | 123 | it('should resolve a JSON pointer-like fragment onto an absolute URL', 124 | function() 125 | { 126 | var from = 'http://google.com/some/search#foo'; 127 | var to = '#/bar/baz/qux'; 128 | var expected = 'http://google.com/some/search#/bar/baz/qux'; 129 | uri.resolve(from, to).should.eql(expected); 130 | }); 131 | 132 | it('should resolve a fragment onto a URN', function() { 133 | var from = 'urn:uuid:FB8E5076-BF6F-4DED-8165-8369A9158B46'; 134 | var to = '#qux'; 135 | var expected = from + to; 136 | uri.resolve(from, to).should.eql(expected); 137 | }); 138 | 139 | it('should resolve a JSON pointer-like fragment onto a URN', function() { 140 | var from = 'urn:uuid:FB8E5076-BF6F-4DED-8165-8369A9158B46'; 141 | var to = '#/bar/baz/qux'; 142 | var expected = from + to; 143 | uri.resolve(from, to).should.eql(expected); 144 | }); 145 | 146 | it('should resolve a root JSON pointer', function() { 147 | var from = 'http://google.com/some#/search'; 148 | var to = '#'; 149 | var expected = 'http://google.com/some#'; 150 | uri.resolve(from, to).should.eql(expected); 151 | }); 152 | 153 | it('should resolve two URNs', function() { 154 | var from = 'urn:foo:bar'; 155 | var to = 'urn:foo:bar'; 156 | var expected = 'urn:foo:bar#'; 157 | uri.resolve(from, to).should.eql(expected); 158 | }); 159 | }); 160 | }); 161 | --------------------------------------------------------------------------------