diff --git a/.eslintrc.yml b/.eslintrc.yml index dfd66150d..9fe920d6a 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -5,7 +5,7 @@ env: rules: block-scoped-var: 2 callback-return: 2 - complexity: [2, 13] + complexity: [2, 16] curly: [2, multi-or-nest, consistent] dot-location: [2, property] dot-notation: 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..88fc551fa --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: epoberezkin +tidelift: "npm/ajv" +open_collective: "ajv" diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 1deda1e23..558501036 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,11 +1,12 @@ @@ -15,7 +16,7 @@ This template is for bug reports. For other issues please use: **Ajv options object** - + ```javascript @@ -48,7 +49,7 @@ This template is for bug reports. For other issues please use: + +**What version of Ajv are you using? Does the issue happen if you use the latest version?** + + + +**Ajv options object** + + + +```javascript + + +``` + + +**JSON Schema** + + + +```json + + +``` + + +**Sample data** + + + +```json + + +``` + + +**Your code** + + + +```javascript + + +``` + + +**Validation result, data AFTER validation, error messages** + +``` + + +``` + +**What results did you expect?** + + +**Are you going to resolve the issue?** diff --git a/.github/ISSUE_TEMPLATE/change.md b/.github/ISSUE_TEMPLATE/change.md new file mode 100644 index 000000000..0c8035d1a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/change.md @@ -0,0 +1,24 @@ +--- +name: Feature or change proposal +about: For proposals of new features, options or some other improvements +title: '' +labels: 'enhancement' +assignees: '' + +--- + + + +**What version of Ajv you are you using?** + +**What problem do you want to solve?** + +**What do you think is the correct solution to problem?** + +**Will you be able to implement it?** diff --git a/.github/ISSUE_TEMPLATE/compatibility.md b/.github/ISSUE_TEMPLATE/compatibility.md new file mode 100644 index 000000000..79aa63999 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/compatibility.md @@ -0,0 +1,28 @@ +--- +name: Browser and compatibility issue +about: For issues that only happen in a specific environment +title: '' +labels: 'compatibility' +assignees: '' + +--- + + + +**The version of Ajv you are using** + +**The environment you have the problem with** + +**Your code (please make it as small as possible to reproduce the issue)** + +**If your issue is in the browser, please list the other packages loaded in the page in the order they are loaded. Please check if the issue gets resolved (or results change) if you move Ajv bundle closer to the top** + +**Results in node.js v8+** + +**Results and error messages in your platform** diff --git a/.github/ISSUE_TEMPLATE/installation.md b/.github/ISSUE_TEMPLATE/installation.md new file mode 100644 index 000000000..1786e9f2f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/installation.md @@ -0,0 +1,33 @@ +--- +name: Installation and dependency issue +about: For issues that happen during installation +title: '' +labels: 'installation' +assignees: '' + +--- + + + +**The version of Ajv you are using** + +**Operating system and node.js version** + +**Package manager and its version** + +**Link to (or contents of) package.json** + +**Error messages** + +**The output of `npm ls`** diff --git a/.github/ISSUE_TEMPLATE/typescript.md b/.github/ISSUE_TEMPLATE/typescript.md new file mode 100644 index 000000000..de3c20168 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/typescript.md @@ -0,0 +1,42 @@ +--- +name: Missing or incorrect type definition +about: Please use for issues related to typescript types +title: '' +labels: 'typescript' +assignees: '' + +--- + + + +**What version of Ajv are you using? Does the issue happen if you use the latest version?** + + +**Your typescript code** + + + +```typescript + + +``` + + +**Typescript compiler error messages** + +``` + + +``` + +**Describe the change that should be made to address the issue?** + + +**Are you going to resolve the issue?** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d7feecdbd..7abf655f1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,7 @@ Thank you for submitting a pull request to Ajv. Before continuing, please read the guidelines: -https://github.com/epoberezkin/ajv/blob/master/CONTRIBUTING.md#pull-requests +https://github.com/ajv-validator/ajv/blob/master/CONTRIBUTING.md#pull-requests If the pull request contains code please make sure there is an issue that we agreed to resolve (if it is a documentation improvement there is no need for an issue). diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 000000000..1f6c1054f --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,32 @@ +# Please supply comments to be used for GitHub labels +githubLabels: + bug: > + Bug confirmed - to be fixed. PR is welcome! + +# duplicate: > +# enhancement: > +# good first issue: > +# help wanted: > +# invalid: > +# question: > +# wont fix: > + + bug report: > + Thank you for the report! If you didn't post a code sample to RunKit yet, + please clone this notebook https://runkit.com/esp/ajv-issue, + post the code sample that demonstrates the bug and post the link here. + It will speed up the investigation and fixing! + + json schema: > + This question is about the usage of JSON Schema specification - it is not specific to Ajv. + Please use JSON Schema reference materials or [submit the question to Stack Overflow](https://stackoverflow.com/questions/ask?tags=jsonschema,ajv). + + - [JSON Schema specification](http://json-schema.org/) + + - [Tutorial by Space Telescope Science Institute](http://json-schema.org/understanding-json-schema/) + + - [validation keywords](https://github.com/ajv-validator/ajv#validation-keywords) (in Ajv docs) + + - [combining schemas](https://github.com/ajv-validator/ajv#ref) (in Ajv docs) + + - [Tutorial by @epoberezkin](https://code.tutsplus.com/tutorials/validating-data-with-json-schema-part-1--cms-25343) diff --git a/.gitignore b/.gitignore index e2a293b92..b7f774a0d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ lib/dotjs/*.js # bundles dist/ + +package-lock.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..43c97e719 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml index 0ada0bbab..80bb5bf49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,14 +3,14 @@ before_script: - git submodule update --init - npm install -g codeclimate-test-reporter node_js: - - "4" - - "6" - - "7" - - "8" + - 10 + - 12 + - 14 after_script: - codeclimate-test-reporter < coverage/lcov.info - coveralls < coverage/lcov.info - scripts/travis-gh-pages + - scripts/publish-built-version notifications: webhooks: urls: diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..410cda641 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at ajv.validator@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/COERCION.md b/COERCION.md index f310c2d67..6a0a41a68 100644 --- a/COERCION.md +++ b/COERCION.md @@ -1,6 +1,6 @@ # Ajv type coercion rules -To enable type coercion pass option `coerceTypes` to Ajv with `true` or `array` (it is `false` by default). See [example](https://github.com/epoberezkin/ajv#coercing-data-types). +To enable type coercion pass option `coerceTypes` to Ajv with `true` or `array` (it is `false` by default). See [example](https://github.com/ajv-validator/ajv#coercing-data-types). The coercion rules are different from JavaScript: - to validate user input as expected diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d6872665e..4f2f8aaed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,9 +5,11 @@ Thank you for your help making Ajv better! Every contribution is appreciated. If - [Documentation](#documentation) - [Issues](#issues) - [Bug reports](#bug-reports) + - [Security vulnerabilities](#security-vulnerabilities) - [Change proposals](#changes) - [Browser and compatibility issues](#compatibility) - - [JSON schema standard](#json-schema) + - [Installation and dependency issues](#installation) + - [JSON Schema standard](#json-schema) - [Ajv usage questions](#usage) - [Code](#code) - [Development](#development) @@ -22,7 +24,7 @@ Ajv has a lot of features and maintaining documentation takes time. I appreciate ## Issues -Before submitting the issue please search the existing issues and also review [Frequently Asked Questions](https://github.com/epoberezkin/ajv/blob/master/FAQ.md). +Before submitting the issue please search the existing issues and also review [Frequently Asked Questions](https://github.com/ajv-validator/ajv/blob/master/FAQ.md). I would really appreciate the time you spend providing all the information and reducing both your schema and data to the smallest possible size when they still have the issue. Simplifying the issue also makes it more valuable for other users (in cases it turns out to be an incorrect usage rather than a bug). @@ -32,18 +34,29 @@ I would really appreciate the time you spend providing all the information and r Please make sure to include the following information in the issue: 1. What version of Ajv are you using? Does the issue happen if you use the latest version? -2. Ajv options object (see https://github.com/epoberezkin/ajv#options). -3. JSON schema and the data you are validating (please make it as small as possible to reproduce the issue). +2. Ajv options object (see https://github.com/ajv-validator/ajv#options). +3. JSON Schema and the data you are validating (please make it as small as possible to reproduce the issue). 4. Your code (please use `options`, `schema` and `data` as variables). 5. Validation result, data AFTER validation, error messages. 6. What results did you expect? -[Create bug report](https://github.com/epoberezkin/ajv/issues/new). +Please include the link to the working code sample at Runkit.com (please clone https://runkit.com/esp/ajv-issue) - it will speed up investigation and fixing. + +[Create bug report](https://github.com/ajv-validator/ajv/issues/new?template=bug-or-error-report.md). + + +#### Security vulnerabilities + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. + +Please do NOT report security vulnerabilities via GitHub issues. #### Change proposals -[Create a proposal](https://github.com/epoberezkin/ajv/issues/new?labels=suggestion&body=**What%20version%20of%20Ajv%20you%20are%20you%20using%3F**%0A%0A**What%20problem%20do%20you%20want%20to%20solve%3F**%0A%0A**What%20do%20you%20think%20is%20the%20correct%20solution%20to%20problem?**%0A%0A**Will%20you%20be%20able%20to%20implement%20it%3F**%0A%0A) for a new feature, option or some other improvement. +[Create a proposal](https://github.com/ajv-validator/ajv/issues/new?template=change.md) for a new feature, option or some other improvement. Please include this information: @@ -63,7 +76,7 @@ Please include as much details as possible. #### Browser and compatibility issues -[Create an issue](https://github.com/epoberezkin/ajv/issues/new?labels=compatibility&body=**The%20version%20of%20Ajv%20you%20are%20using**%0A%0A**The%20environment%20you%20have%20the%20problem%20with.**%0A%0A**Your%20code%20(please%20make%20it%20as%20small%20as%20possible%20to%20reproduce%20the%20issue).**%0A%0A**If%20your%20issue%20is%20in%20the%20browser,%20please%20list%20the%20other%20packages%20loaded%20in%20the%20page%20in%20the%20order%20they%20are%20loaded.%20Please%20check%20if%20the%20issue%20gets%20resolved%20(or%20results%20change)%20if%20you%20move%20Ajv%20bundle%20closer%20to%20the%20top.**%0A%0A**Results%20in%20node.js%20v4.**%0A%0A**Results%20and%20error%20messages%20in%20your%20platform.**%0A%0A) to report a compatibility problem that only happens in a particular environemnt (when your code works correctly in node.js v4 in linux systems but fails in some other environment). +[Create an issue](https://github.com/ajv-validator/ajv/issues/new?template=compatibility.md) to report a compatibility problem that only happens in a particular environment (when your code works correctly in node.js v8+ in linux systems but fails in some other environment). Please include this information: @@ -71,17 +84,34 @@ Please include this information: 2. The environment you have the problem with. 3. Your code (please make it as small as possible to reproduce the issue). 4. If your issue is in the browser, please list the other packages loaded in the page in the order they are loaded. Please check if the issue gets resolved (or results change) if you move Ajv bundle closer to the top. -5. Results in node.js v4. +5. Results in node.js v8+. 6. Results and error messages in your platform. -#### Using JSON schema standard +#### Installation and dependency issues + +[Create an issue](https://github.com/ajv-validator/ajv/issues/new?template=installation.md) to report problems that happen during Ajv installation or when Ajv is missing some dependency. + +Before submitting the issue, please try the following: +- use the latest stable Node.js and `npm` +- use `yarn` instead of `npm` - the issue can be related to https://github.com/npm/npm/issues/19877 +- remove `node_modules` and `package-lock.json` and run install again + +If nothing helps, please submit: + +1. The version of Ajv you are using +2. Operating system and node.js version +3. Package manager and its version +4. Link to (or contents of) package.json +5. Error messages +6. The output of `npm ls` + -Ajv implements JSON schema standard draft 4 and the proposed extensions for the next version of the standard (available when you use the option `v5: true`). +#### Using JSON Schema standard -If the issue is related to using v5 extensions please submit it as a [bug report](https://github.com/epoberezkin/ajv/issues/new). +Ajv implements JSON Schema standard draft-04 and draft-06/07. -If it is a general issue related to using the standard keywords included in JSON Schema or implementing some advanced validation logic please ask the question on [Stack Overflow](http://stackoverflow.com/questions/ask?tags=jsonschema,ajv) (my account is [esp](http://stackoverflow.com/users/1816503/esp)) or submitting the question to [JSON-Schema.org](https://github.com/json-schema-org/json-schema-spec/issues/new). Please mention @epoberezkin. +If it is a general issue related to using the standard keywords included in JSON Schema or implementing some advanced validation logic please ask the question on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=jsonschema,ajv) (my account is [esp](https://stackoverflow.com/users/1816503/esp)) or submitting the question to [JSON-Schema.org](https://github.com/json-schema-org/json-schema-spec/issues/new). Please mention @epoberezkin. #### Ajv usage questions @@ -113,9 +143,9 @@ npm run test-fast git commit -nm 'type: message' ``` -All validation functions are generated using doT templates in [dot](https://github.com/epoberezkin/ajv/tree/master/lib/dot) folder. Templates are precompiled so doT is not a run-time dependency. +All validation functions are generated using doT templates in [dot](https://github.com/ajv-validator/ajv/tree/master/lib/dot) folder. Templates are precompiled so doT is not a run-time dependency. -`npm run build` - compiles templates to [dotjs](https://github.com/epoberezkin/ajv/tree/master/lib/dotjs) folder. +`npm run build` - compiles templates to [dotjs](https://github.com/ajv-validator/ajv/tree/master/lib/dotjs) folder. `npm run watch` - automatically compiles templates when files in dot folder change @@ -124,7 +154,7 @@ All validation functions are generated using doT templates in [dot](https://gith To make accepting your changes faster please follow these steps: -1. Submit an [issue with the bug](https://github.com/epoberezkin/ajv/issues/new) or with the proposed change (unless the contribution is to fix the documentation typos and mistakes). +1. Submit an [issue with the bug](https://github.com/ajv-validator/ajv/issues/new) or with the proposed change (unless the contribution is to fix the documentation typos and mistakes). 2. Please describe the proposed api and implementation plan (unless the issue is a relatively simple bug and fixing it doesn't change any api). 3. Once agreed, please write as little code as possible to achieve the desired result. 4. Please avoid unnecessary changes, refactoring or changing coding styles as part of your change (unless the change was proposed as refactoring). diff --git a/CUSTOM.md b/CUSTOM.md index 6d32f40bf..68cac5ab3 100644 --- a/CUSTOM.md +++ b/CUSTOM.md @@ -34,26 +34,35 @@ This way to define keywords is useful for: - testing your keywords before converting them to compiled/inlined keywords - defining keywords that do not depend on the schema value (e.g., when the value is always `true`). In this case you can add option `schema: false` to the keyword definition and the schemas won't be passed to the validation function, it will only receive the same 4 parameters as compiled validation function (see the next section). - defining keywords where the schema is a value used in some expression. -- defining keywords that support [$data reference](https://github.com/epoberezkin/ajv#data-reference) - in this case validation function is required, either as the only option or in addition to compile, macro or inline function (see below). +- defining keywords that support [$data reference](https://github.com/ajv-validator/ajv#data-reference) - in this case validation function is required, either as the only option or in addition to compile, macro or inline function (see below). __Please note__: In cases when validation flow is different depending on the schema and you have to use `if`s, this way to define keywords will have worse performance than compiled keyword returning different validation functions depending on the schema. -Example. `constant` keyword (a synonym for draft6 keyword `const`, it is equivalent to `enum` keyword with one item): +Example. `constant` keyword (a synonym for draft-06 keyword `const`, it is equivalent to `enum` keyword with one item): ```javascript -ajv.addKeyword('constant', { validate: function (schema, data) { - return typeof schema == 'object' && schema !== null - ? deepEqual(schema, data) - : schema === data; -}, errors: false }); +ajv.addKeyword('constant', { + validate: function (schema, data) { + return typeof schema == 'object' && schema !== null + ? deepEqual(schema, data) + : schema === data; + }, + errors: false +}); -var schema = { "constant": 2 }; +var schema = { + "constant": 2 +}; var validate = ajv.compile(schema); console.log(validate(2)); // true console.log(validate(3)); // false -var schema = { "constant": { "foo": "bar" } }; +var schema = { + "constant": { + "foo": "bar" + } +}; var validate = ajv.compile(schema); console.log(validate({foo: 'bar'})); // true console.log(validate({foo: 'baz'})); // false @@ -79,27 +88,40 @@ The access to the parent data object and the current property name allow to crea The function should return validation result as boolean. It can return an array of validation errors via `.errors` property of itself (otherwise a standard error will be used). -In some cases it is the best approach to define keywords, but it has the performance cost of an extra function call during validation. If keyword logic can be expressed via some other JSON-schema then `macro` keyword definition is more efficient (see below). +In some cases it is the best approach to define keywords, but it has the performance cost of an extra function call during validation. If keyword logic can be expressed via some other JSON Schema then `macro` keyword definition is more efficient (see below). All custom keywords types can have an optional `metaSchema` property in their definitions. It is a schema against which the value of keyword will be validated during schema compilation. +Custom keyword can also have an optional `dependencies` property in their definitions - it is a list of required keywords in a containing (parent) schema. + Example. `range` and `exclusiveRange` keywords using compiled schema: ```javascript -ajv.addKeyword('range', { type: 'number', compile: function (sch, parentSchema) { - var min = sch[0]; - var max = sch[1]; - - return parentSchema.exclusiveRange === true - ? function (data) { return data > min && data < max; } - : function (data) { return data >= min && data <= max; } -}, errors: false, metaSchema: { - type: 'array', - items: [ { type: 'number' }, { type: 'number' } ], - additionalItems: false -} }); +ajv.addKeyword('range', { + type: 'number', + compile: function (sch, parentSchema) { + var min = sch[0]; + var max = sch[1]; + + return parentSchema.exclusiveRange === true + ? function (data) { return data > min && data < max; } + : function (data) { return data >= min && data <= max; } + }, + errors: false, + metaSchema: { + type: 'array', + items: [ + { type: 'number' }, + { type: 'number' } + ], + additionalItems: false + } +}); -var schema = { "range": [2, 4], "exclusiveRange": true }; +var schema = { + "range": [2, 4], + "exclusiveRange": true +}; var validate = ajv.compile(schema); console.log(validate(2.01)); // true console.log(validate(3.99)); // true @@ -114,7 +136,7 @@ See note on custom errors and asynchronous keywords in the previous section. "Macro" function is called during schema compilation. It is passed schema, parent schema and [schema compilation context](#schema-compilation-context) and it should return another schema that will be applied to the data in addition to the original schema. -It is the most efficient approach (in cases when the keyword logic can be expressed with another JSON-schema) because it is usually easy to implement and there is no extra function call during validation. +It is the most efficient approach (in cases when the keyword logic can be expressed with another JSON Schema) because it is usually easy to implement and there is no extra function call during validation. In addition to the errors from the expanded schema macro keyword will add its own error in case validation fails. @@ -122,27 +144,30 @@ In addition to the errors from the expanded schema macro keyword will add its ow Example. `range` and `exclusiveRange` keywords from the previous example defined with macro: ```javascript -ajv.addKeyword('range', { type: 'number', macro: function (schema, parentSchema) { - return { - minimum: schema[0], - maximum: schema[1], - exclusiveMinimum: !!parentSchema.exclusiveRange, - exclusiveMaximum: !!parentSchema.exclusiveRange - }; -}, metaSchema: { - type: 'array', - items: [ { type: 'number' }, { type: 'number' } ], - additionalItems: false -} }); +ajv.addKeyword('range', { + type: 'number', + macro: function (schema, parentSchema) { + return { + minimum: schema[0], + maximum: schema[1], + exclusiveMinimum: !!parentSchema.exclusiveRange, + exclusiveMaximum: !!parentSchema.exclusiveRange + }; + }, + metaSchema: { + type: 'array', + items: [ + { type: 'number' }, + { type: 'number' } + ], + additionalItems: false + } +}); ``` Example. `contains` keyword from version 5 proposals that requires that the array has at least one item matching schema (see https://github.com/json-schema/json-schema/wiki/contains-(v5-proposal)): ```javascript -ajv.addKeyword('contains', { type: 'array', macro: function (schema) { - return { "not": { "items": { "not": schema } } }; -} }); - var schema = { "contains": { "type": "number", @@ -151,7 +176,20 @@ var schema = { } }; -var validate = ajv.compile(schema); +var validate = ajv.addKeyword('contains', { + type: 'array', + macro: function (schema) { + return { + "not": { + "items": { + "not": schema + } + } + }; + } +}) +.compile(schema); + console.log(validate([1,2,3])); // false console.log(validate([2,3,4])); // false console.log(validate([3,4,5])); // true, number 5 matches schema inside "contains" @@ -159,7 +197,7 @@ console.log(validate([3,4,5])); // true, number 5 matches schema inside "contain `contains` keyword is already available in Ajv with option `v5: true`. -See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/epoberezkin/ajv/blob/master/spec/custom.spec.js#L151). +See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/ajv-validator/ajv/blob/master/spec/custom.spec.js#L151). ### Define keyword with "inline" compilation function @@ -177,14 +215,18 @@ While it can be more challenging to define keywords with "inline" functions, it Example `even` keyword: ```javascript -ajv.addKeyword('even', { type: 'number', inline: function (it, keyword, schema) { - var op = schema ? '===' : '!=='; - return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0'; -}, metaSchema: { type: 'boolean' } }); - var schema = { "even": true }; -var validate = ajv.compile(schema); +var validate = ajv.addKeyword('even', { + type: 'number', + inline: function (it, keyword, schema) { + var op = schema ? '===' : '!=='; + return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0'; + }, + metaSchema: { type: 'boolean' } +}) +.compile(schema); + console.log(validate(2)); // true console.log(validate(3)); // false ``` @@ -214,7 +256,10 @@ ajv.addKeyword('range', { statements: true, metaSchema: { type: 'array', - items: [ { type: 'number' }, { type: 'number' } ], + items: [ + { type: 'number' }, + { type: 'number' } + ], additionalItems: false } }); @@ -249,7 +294,7 @@ The first parameter passed to inline keyword compilation function (and the 3rd p - _opts_ - Ajv instance option. You should not be changing them. - _formats_ - all formats available in Ajv instance, including the custom ones. - _compositeRule_ - boolean indicating that the current schema is inside the compound keyword where failing some rule doesn't mean validation failure (`anyOf`, `oneOf`, `not`, `if` in `switch`). This flag is used to determine whether you can return validation result immediately after any error in case the option `allErrors` is not `true. You only need to do it if you have many steps in your keywords and potentially can define multiple errors. -- _validate_ - the function you need to use to compile subschemas in your keywords (see the [implementation](https://github.com/epoberezkin/ajv/blob/master/lib/dot/v5/switch.jst) of `switch` keyword for example). +- _validate_ - the function you need to use to compile subschemas in your keywords (see the [implementation](https://github.com/ajv-validator/ajv-keywords/blob/master/keywords/dot/switch.jst) of `switch` keyword for example). - _util_ - [Ajv utilities](#ajv-utilities) you can use in your inline compilation functions. - _self_ - Ajv instance. @@ -266,8 +311,8 @@ There is a number of variables and expressions you can use in the generated (val - `'validate.schema' + it.schemaPath` - current level schema available at validation time (the same schema at compile time is `it.schema`). - `'validate.schema' + it.schemaPath + '.' + keyword` - the value of your custom keyword at validation-time. Keyword is passed as the second parameter to the inline compilation function to allow using the same function to compile multiple keywords. - `'valid' + it.level` - the variable that you have to declare and to assign the validation result to if your keyword returns statements rather than expression (`statements: true`). -- `'errors'` - the number of encountered errors. See [Reporting errors in custom keywords](https://github.com/epoberezkin/ajv/blob/master/CUSTOM.md#reporting-errors-in-custom-keywords). -- `'vErrors'` - the array with errors collected so far. See [Reporting errors in custom keywords](https://github.com/epoberezkin/ajv/blob/master/CUSTOM.md#reporting-errors-in-custom-keywords). +- `'errors'` - the number of encountered errors. See [Reporting errors in custom keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md#reporting-errors-in-custom-keywords). +- `'vErrors'` - the array with errors collected so far. See [Reporting errors in custom keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md#reporting-errors-in-custom-keywords). ## Ajv utilities @@ -365,7 +410,7 @@ All custom keywords but macro keywords can optionally create custom error messag Synchronous validating and compiled keywords should define errors by assigning them to `.errors` property of the validation function. Asynchronous keywords can return promise that rejects with `new Ajv.ValidationError(errors)`, where `errors` is an array of custom validation errors (if you don't want to define custom errors in asynchronous keyword, its validation function can return the promise that resolves with `false`). -Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). This can be done for both synchronous and asynchronous keywords. See [example range keyword](https://github.com/epoberezkin/ajv/blob/master/spec/custom_rules/range_with_errors.jst). +Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). This can be done for both synchronous and asynchronous keywords. See [example range keyword](https://github.com/ajv-validator/ajv/blob/master/spec/custom_rules/range_with_errors.jst). When inline keyword performs validation Ajv checks whether it created errors by comparing errors count before and after validation. To skip this check add option `errors` (can be `"full"`, `true` or `false`) to keyword definition: @@ -384,7 +429,7 @@ Each error object should at least have properties `keyword`, `message` and `para Inlined keywords can optionally define `dataPath` and `schemaPath` properties in error objects, that will be assigned by Ajv unless `errors` option of the keyword is `"full"`. -If custom keyword doesn't create errors, the default error will be created in case the keyword fails validation (see [Validation errors](https://github.com/epoberezkin/ajv#validation-errors)). +If custom keyword doesn't create errors, the default error will be created in case the keyword fails validation (see [Validation errors](https://github.com/ajv-validator/ajv#validation-errors)). ## Short-circuit validation diff --git a/FAQ.md b/FAQ.md index 41c985a47..f010a51c8 100644 --- a/FAQ.md +++ b/FAQ.md @@ -3,9 +3,30 @@ The purpose of this document is to help find answers quicker. I am happy to continue the discussion about these issues, so please comment on some of the issues mentioned below or create a new issue if it seems more appropriate. + +## Using JSON schema + +Ajv implements JSON schema specification. Before submitting the issue about the behaviour of any validation keywords please review them in: + +- [JSON Schema specification](https://tools.ietf.org/html/draft-handrews-json-schema-validation-00) (draft-07) +- [Validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md) in Ajv documentation +- [JSON Schema tutorial](https://spacetelescope.github.io/understanding-json-schema/) (for draft-04) + + +##### Why Ajv validates empty object as valid? + +"properties" keyword does not require the presence of any properties, you need to use "required" keyword. It also doesn't require that the data is an object, so any other type of data will also be valid. To require a specific type use "type" keyword. + + +##### Why Ajv validates only the first item of the array? + +"items" keyword support [two syntaxes](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#items) - 1) when the schema applies to all items; 2) when there is a different schema for each item in the beginning of the array. This problem means you are using the second syntax. + + + ## Ajv API for returning validation errors -See [#65](https://github.com/epoberezkin/ajv/issues/65), [#212](https://github.com/epoberezkin/ajv/issues/212), [#236](https://github.com/epoberezkin/ajv/issues/236), [#242](https://github.com/epoberezkin/ajv/issues/242), [#256](https://github.com/epoberezkin/ajv/issues/256). +See [#65](https://github.com/ajv-validator/ajv/issues/65), [#212](https://github.com/ajv-validator/ajv/issues/212), [#236](https://github.com/ajv-validator/ajv/issues/236), [#242](https://github.com/ajv-validator/ajv/issues/242), [#256](https://github.com/ajv-validator/ajv/issues/256). ##### Why Ajv assigns errors as a property of validation function (or instance) instead of returning an object with validation results and errors? @@ -29,17 +50,18 @@ No. In many cases there is a module responsible for the validation in the applic Doing this would create a precedent where validated data is used in error messages, creating a vulnerability (e.g., when ajv is used to validate API data/parameters and error messages are logged). -Since the property name is already in the params object, in an application you can modify messages in any way you need. ajv-errors package will allow to modify messages as well - templating is [not there yet](https://github.com/epoberezkin/ajv-errors/issues/4), though. +Since the property name is already in the params object, in an application you can modify messages in any way you need. ajv-errors package allows modifying messages as well. + ## Additional properties inside compound keywords anyOf, oneOf, etc. -See [#127](https://github.com/epoberezkin/ajv/issues/127), [#129](https://github.com/epoberezkin/ajv/issues/129), [#134](https://github.com/epoberezkin/ajv/issues/134), [#140](https://github.com/epoberezkin/ajv/issues/140), [#193](https://github.com/epoberezkin/ajv/issues/193), [#205](https://github.com/epoberezkin/ajv/issues/205), [#238](https://github.com/epoberezkin/ajv/issues/238), [#264](https://github.com/epoberezkin/ajv/issues/264). +See [#127](https://github.com/ajv-validator/ajv/issues/127), [#129](https://github.com/ajv-validator/ajv/issues/129), [#134](https://github.com/ajv-validator/ajv/issues/134), [#140](https://github.com/ajv-validator/ajv/issues/140), [#193](https://github.com/ajv-validator/ajv/issues/193), [#205](https://github.com/ajv-validator/ajv/issues/205), [#238](https://github.com/ajv-validator/ajv/issues/238), [#264](https://github.com/ajv-validator/ajv/issues/264). ##### Why the keyword `additionalProperties: false` fails validation when some properties are "declared" inside a subschema in `anyOf`/etc.? -The keyword `additionalProperties` creates the restriction on validated data based on its own value (`false` or schema object) and on the keywords `properties` and `patternProperties` in the SAME schema object. JSON-schema validators must NOT take into account properties used in other schema objects. +The keyword `additionalProperties` creates the restriction on validated data based on its own value (`false` or schema object) and on the keywords `properties` and `patternProperties` in the SAME schema object. JSON Schema validators must NOT take into account properties used in other schema objects. While you can expect that the schema below would allow the objects either with properties `foo` and `bar` or with properties `foo` and `baz` and all other properties will be prohibited, this schema will only allow objects with one property `foo` (an empty object and any non-objects will also be valid): @@ -61,14 +83,15 @@ There are several ways to implement the described logic that would allow two pro ##### Why the validation fails when I use option `removeAdditional` with the keyword `anyOf`/etc.? -This problem is related to the problem explained above - properties treated as additional in the sence of `additionalProperties` keyword, based on `properties`/`patternProperties` keyword in the same schema object. +This problem is related to the problem explained above - properties treated as additional in the sense of `additionalProperties` keyword, based on `properties`/`patternProperties` keyword in the same schema object. + +See the exemple in [Filtering Data](https://github.com/ajv-validator/ajv#filtering-data) section of readme. -See the exemple in [Filtering Data](https://github.com/epoberezkin/ajv#filtering-data) section of readme. ## Generating schemas with resolved references ($ref) -See [#22](https://github.com/epoberezkin/ajv/issues/22), [#125](https://github.com/epoberezkin/ajv/issues/125), [#146](https://github.com/epoberezkin/ajv/issues/146), [#228](https://github.com/epoberezkin/ajv/issues/228), [#336](https://github.com/epoberezkin/ajv/issues/336), [#454](https://github.com/epoberezkin/ajv/issues/454). +See [#22](https://github.com/ajv-validator/ajv/issues/22), [#125](https://github.com/ajv-validator/ajv/issues/125), [#146](https://github.com/ajv-validator/ajv/issues/146), [#228](https://github.com/ajv-validator/ajv/issues/228), [#336](https://github.com/ajv-validator/ajv/issues/336), [#454](https://github.com/ajv-validator/ajv/issues/454). ##### Why Ajv does not replace references ($ref) with the actual referenced schemas as some validators do? @@ -77,12 +100,12 @@ See [#22](https://github.com/epoberezkin/ajv/issues/22), [#125](https://github.c 2. When schemas are recursive (or mutually recursive) resolving references would result in self-referencing recursive data-structures that can be difficult to process. 3. There are cases when such inlining would also require adding (or modyfing) `id` attribute in the inlined schema fragment to make the resulting schema equivalent. -There were many conversations about the meaning of `$ref` in [JSON Schema GitHub organisation](https://github.com/json-schema-org). The consesus is that while it is possible to treat `$ref` as schema inclusion with two caveats (above), this interpretation is unnecessary complex. A more efficient approach is to treat `$ref` as a delegation, i.e. a special keyword that validates the current data instance against the referenced schema. The analogy with programming languages is that `$ref` is a function call rather than a macro. See [here](https://github.com/json-schema-org/json-schema-spec/issues/279), for example. +There were many conversations about the meaning of `$ref` in [JSON Schema GitHub organisation](https://github.com/json-schema-org). The consensus is that while it is possible to treat `$ref` as schema inclusion with two caveats (above), this interpretation is unnecessary complex. A more efficient approach is to treat `$ref` as a delegation, i.e. a special keyword that validates the current data instance against the referenced schema. The analogy with programming languages is that `$ref` is a function call rather than a macro. See [here](https://github.com/json-schema-org/json-schema-spec/issues/279), for example. ##### How can I generate a schema where `$ref` keywords are replaced with referenced schemas? There are two possible approaches: -1. Write code to traverse schema and replace every `$ref` with the referenced schema. An additional limitation is that `"$ref"` inside keywords "properties", "patternProperties" and "dependencies" means property name (or pattern) rather than the reference to another schema. -2. Use a specially constructed JSON Schema with a [custom keyword](https://github.com/epoberezkin/ajv/blob/master/CUSTOM.md) to traverse and modify your schema. +1. Traverse schema (e.g. with json-schema-traverse) and replace every `$ref` with the referenced schema. +2. Use a specially constructed JSON Schema with a [custom keyword](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md) to traverse and modify your schema. diff --git a/KEYWORDS.md b/KEYWORDS.md index c53e15e95..6601a9a1b 100644 --- a/KEYWORDS.md +++ b/KEYWORDS.md @@ -1,7 +1,7 @@ # JSON Schema validation keywords -In a simple way, JSON schema is an object with validation keywords. +In a simple way, JSON Schema is an object with validation keywords. The keywords and their values define what rules the data should satisfy to be valid. @@ -10,7 +10,7 @@ The keywords and their values define what rules the data should satisfy to be va - [type](#type) - [Keywords for numbers](#keywords-for-numbers) - - [maximum / minimum and exclusiveMaximum / exclusiveMinimum](#maximum--minimum-and-exclusivemaximum--exclusiveminimum) (CHANGED in draft 6) + - [maximum / minimum and exclusiveMaximum / exclusiveMinimum](#maximum--minimum-and-exclusivemaximum--exclusiveminimum) (changed in draft-06) - [multipleOf](#multipleof) - [Keywords for strings](#keywords-for-strings) - [maxLength/minLength](#maxlength--minlength) @@ -22,7 +22,7 @@ The keywords and their values define what rules the data should satisfy to be va - [uniqueItems](#uniqueitems) - [items](#items) - [additionalItems](#additionalitems) - - [contains](#contains) (NEW in draft 6) + - [contains](#contains) (added in draft-06) - [Keywords for objects](#keywords-for-objects) - [maxProperties/minProperties](#maxproperties--minproperties) - [required](#required) @@ -30,18 +30,17 @@ The keywords and their values define what rules the data should satisfy to be va - [patternProperties](#patternproperties) - [additionalProperties](#additionalproperties) - [dependencies](#dependencies) - - [propertyNames](#propertynames) (NEW in draft 6) - - [patternGroups](#patterngroups-deprecated) (deprecated) + - [propertyNames](#propertynames) (added in draft-06) - [patternRequired](#patternrequired-proposed) (proposed) - [Keywords for all types](#keywords-for-all-types) - [enum](#enum) - - [const](#const) (NEW in draft 6) + - [const](#const) (added in draft-06) - [Compound keywords](#compound-keywords) - [not](#not) - [oneOf](#oneof) - [anyOf](#anyof) - [allOf](#allof) - - [switch](#switch-proposed) (proposed) + - [if/then/else](#ifthenelse) (NEW in draft-07) @@ -49,7 +48,7 @@ The keywords and their values define what rules the data should satisfy to be va `type` keyword requires that the data is of certain type (or some of types). Its value can be a string (the allowed type) or an array of strings (multiple allowed types). -Type can be: number, integer, string, boolean, array, object or null. +Type can be: `number`, `integer`, `string`, `boolean`, `array`, `object` or `null`. __Examples__ @@ -75,7 +74,7 @@ __Examples__ _invalid_: `[]`, `{}`, `null`, `true` -All examples above are JSON schemas that only require data to be of certain type to be valid. +All examples above are JSON Schemas that only require data to be of certain type to be valid. Most other keywords apply only to a particular type of data. If the data is of different type, the keyword will not apply and the data will be considered valid. @@ -88,11 +87,11 @@ Most other keywords apply only to a particular type of data. If the data is of d The value of keyword `maximum` (`minimum`) should be a number. This value is the maximum (minimum) allowed value for the data to be valid. -Draft 4: The value of keyword `exclusiveMaximum` (`exclusiveMinimum`) should be a boolean value. These keyword cannot be used without `maximum` (`minimum`). If this keyword value is equal to `true`, the data should not be equal to the value in `maximum` (`minimum`) keyword to be valid. +Draft-04: The value of keyword `exclusiveMaximum` (`exclusiveMinimum`) should be a boolean value. These keyword cannot be used without `maximum` (`minimum`). If this keyword value is equal to `true`, the data should not be equal to the value in `maximum` (`minimum`) keyword to be valid. -Draft 6: The value of keyword `exclusiveMaximum` (`exclusiveMinimum`) should be a number. This value is the exclusive maximum (minimum) allowed value for the data to be valid (the data equal to this keyword value is invalid). +Draft-06/07: The value of keyword `exclusiveMaximum` (`exclusiveMinimum`) should be a number. This value is the exclusive maximum (minimum) allowed value for the data to be valid (the data equal to this keyword value is invalid). -Ajv supports both draft 4 and draft 6 syntaxes. +Ajv supports both draft-04 and draft-06/07 syntaxes. __Examples__ @@ -112,8 +111,8 @@ __Examples__ 3. _schema_: - draft 4: `{ "minimum": 5, "exclusiveMinimum": true }` - draft 6: `{ "exclusiveMinimum": 5 }` + draft-04: `{ "minimum": 5, "exclusiveMinimum": true }` + draft-06/07: `{ "exclusiveMinimum": 5 }` _valid_: `6`, `7`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) @@ -203,7 +202,7 @@ _invalid_: `"abc"` ### `formatMaximum` / `formatMinimum` and `formatExclusiveMaximum` / `formatExclusiveMinimum` (proposed) -Defined in [ajv-keywords](https://github.com/epoberezkin/ajv-keywords) package. +Defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package. The value of keyword `formatMaximum` (`formatMinimum`) should be a string. This value is the maximum (minimum) allowed value for the data to be valid as determined by `format` keyword. @@ -268,7 +267,7 @@ The value of the keyword should be an object or an array of objects. If the keyword value is an object, then for the data array to be valid each item of the array should be valid according to the schema in this value. In this case the "additionalItems" keyword is ignored. -If the keyword value is an array, then items with indeces less than the number of items in the keyword should be valid according to the schemas with the same indeces. Whether additional items are valid will depend on "additionalItems" keyword. +If the keyword value is an array, then items with indices less than the number of items in the keyword should be valid according to the schemas with the same indices. Whether additional items are valid will depend on "additionalItems" keyword. __Examples__ @@ -308,7 +307,7 @@ If the length of data array is bigger than the length of "items" keyword value t - `false`: data is invalid - `true`: data is valid -- an object: data is valid if all additional items (i.e. items with indeces greater or equal than "items" keyword value length) are valid according to the schema in "assitionalItems" keyword. +- an object: data is valid if all additional items (i.e. items with indices greater or equal than "items" keyword value length) are valid according to the schema in "additionalItems" keyword. __Examples__ @@ -365,7 +364,7 @@ __Examples__ ### `contains` -The value of the keyword is a JSON-schema. The array is valid if it contains at least one item that is valid according to this schema. +The value of the keyword is a JSON Schema. The array is valid if it contains at least one item that is valid according to this schema. __Example__ @@ -424,7 +423,7 @@ _invalid_: `{}`, `{"a": 1}`, `{"c": 3, "d":4}` ### `properties` -The value of the keyword should be a map with keys equal to data object properties. Each value in the map should be a JSON schema. For data object to be valid the corresponding values in data object properties should be valid according to these schemas. +The value of the keyword should be a map with keys equal to data object properties. Each value in the map should be a JSON Schema. For data object to be valid the corresponding values in data object properties should be valid according to these schemas. __Please note__: `properties` keyword does not require that the properties mentioned in it are present in the object (see examples). @@ -451,7 +450,7 @@ _invalid_: `{"foo": 1}`, `{"foo": "a", "bar": 1}` ### `patternProperties` -The value of this keyword should be a map where keys should be regular expressions and the values should be JSON schemas. For data object to be valid the values in data object properties that match regular expression(s) should be valid according to the corresponding schema(s). +The value of this keyword should be a map where keys should be regular expressions and the values should be JSON Schemas. For data object to be valid the values in data object properties that match regular expression(s) should be valid according to the corresponding schema(s). When the value in data object property matches multiple regular expressions it should be valid according to all the schemas for all matched regular expressions. @@ -478,7 +477,7 @@ _invalid_: `{"foo": 1}`, `{"foo": "a", "bar": "b"}` ### `additionalProperties` -The value of the keyword should be either a boolean or a JSON schema. +The value of the keyword should be either a boolean or a JSON Schema. If the value is `true` the keyword is ignored. @@ -552,7 +551,7 @@ __Examples__ ### `dependencies` -The value of the keyword is a map with keys equal to data object properties. Each value in the map should be either an array of unique property names ("property dependency") or a JSON schema ("schema dependency"). +The value of the keyword is a map with keys equal to data object properties. Each value in the map should be either an array of unique property names ("property dependency") or a JSON Schema ("schema dependency"). For property dependency, if the data object contains a property that is a key in the keyword value, then to be valid the data object should also contain all properties from the array of properties. @@ -596,7 +595,7 @@ __Examples__ ### `propertyNames` -The value of this keyword is a JSON schema. +The value of this keyword is a JSON Schema. For data object to be valid each property name in this object should be valid according to this schema. @@ -617,44 +616,9 @@ _invalid_: `{"foo": "any value"}` -### `patternGroups` (deprecated) - -This keyword is only provided for backward compatibility, it will be removed in the next major version. To use it, pass option `patternGroups: true`. - -The value of this keyword should be a map where keys should be regular expressions and the values should be objects with the following properties: - -- `schema` (required) - should be a JSON schema. For data object to be valid the values in data object properties that match regular expression(s) should be valid according to the corresponding `schema`(s). -- `maximum` / `minimum` (optional) - should be integers. For data object to be valid the number of properties that match regular expression(s) should be within limits set by `minimum`(s) and `maximum`(s). - - -__Example__ - -_schema_: - -```json -{ - "patternGroups": { - "^[a-z]+$": { - "minimum": 1, - "schema": { "type": "string" } - }, - "^[0-9]+$": { - "minimum": 1, - "schema": { "type": "integer" } - } - } -} -``` - -_valid_: `{ "foo": "bar", "1": "2" }`, any non-object - -_invalid_: `{}`, `{ "foo": "bar" }`, `{ "1": "2" }` - - - ### `patternRequired` (proposed) -Defined in [ajv-keywords](https://github.com/epoberezkin/ajv-keywords) package. +Defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package. The value of this keyword should be an array of strings, each string being a regular expression. For data object to be valid each regular expression in this array should match at least one property name in the data object. @@ -706,7 +670,7 @@ _valid_: `"foo"` _invalid_: any other value -The same can be achieved with `enum` keyword using the array with one item. But `const` keyword is more than just a syntax sugar for `enum`. In combination with the [$data reference](https://github.com/epoberezkin/ajv#data-reference) it allows to define equality relations between different parts of the data. This cannot be achieved with `enum` keyword even with `$data` reference because `$data` cannot be used in place of one item - it can only be used in place of the whole array in `enum` keyword. +The same can be achieved with `enum` keyword using the array with one item. But `const` keyword is more than just a syntax sugar for `enum`. In combination with the [$data reference](https://github.com/ajv-validator/ajv#data-reference) it allows to define equality relations between different parts of the data. This cannot be achieved with `enum` keyword even with `$data` reference because `$data` cannot be used in place of one item - it can only be used in place of the whole array in `enum` keyword. __Example__ @@ -732,7 +696,7 @@ _invalid_: `{ "foo": 1 }`, `{ "bar": 1 }`, `{ "foo": 1, "bar": 2 }` ### `not` -The value of the keyword should be a JSON schema. The data is valid if it is invalid according to this schema. +The value of the keyword should be a JSON Schema. The data is valid if it is invalid according to this schema. __Examples__ @@ -763,7 +727,7 @@ __Examples__ ### `oneOf` -The value of the keyword should be an array of JSON schemas. The data is valid if it matches exactly one JSON schema from this array. Validators have to validate data against all schemas to establish validity according to this keyword. +The value of the keyword should be an array of JSON Schemas. The data is valid if it matches exactly one JSON Schema from this array. Validators have to validate data against all schemas to establish validity according to this keyword. __Example__ @@ -786,7 +750,7 @@ _invalid_: `2`, `3`, `4.5`, `5.5` ### `anyOf` -The value of the keyword should be an array of JSON schemas. The data is valid if it is valid according to one or more JSON schemas in this array. Validators only need to validate data against schemas in order until the first schema matches (or until all schemas have been tried). For this reason validating against this keyword is faster than against "oneOf" keyword in most cases. +The value of the keyword should be an array of JSON Schemas. The data is valid if it is valid according to one or more JSON Schemas in this array. Validators only need to validate data against schemas in order until the first schema matches (or until all schemas have been tried). For this reason validating against this keyword is faster than against "oneOf" keyword in most cases. __Example__ @@ -809,7 +773,7 @@ _invalid_: `4.5`, `5.5` ### `allOf` -The value of the keyword should be an array of JSON schemas. The data is valid if it is valid according to all JSON schemas in this array. +The value of the keyword should be an array of JSON Schemas. The data is valid if it is valid according to all JSON Schemas in this array. __Example__ @@ -830,29 +794,15 @@ _invalid_: `1.5`, `2.5`, `4`, `4.5`, `5`, `5.5`, any non-number -### `switch` (proposed) - -Defined in [ajv-keywords](https://github.com/epoberezkin/ajv-keywords) package. +### `if`/`then`/`else` -The value of the keyword is the array of if/then clauses. Each clause is the object with the following properties: +These keywords allow to implement conditional validation. Their values should be valid JSON Schemas (object or boolean). -- `if` (optional) - the value is JSON-schema -- `then` (required) - the value is JSON-schema or boolean -- `continue` (optional) - the value is boolean +If `if` keyword is absent, the validation succeds. -The validation process is dynamic; all clauses are executed sequentially in the following way: +If the data is valid against the sub-schema in `if` keyword, then the validation result is equal to the result of data validation against the sub-schema in `then` keyword (if `then` is absent, the validation succeeds). -1. `if`: - 1. `if` property is JSON-schema according to which the data is: - 1. valid => go to step 2. - 2. invalid => go to the NEXT clause, if this was the last clause the validation of `switch` SUCCEEDS. - 2. `if` property is absent => go to step 2. -2. `then`: - 1. `then` property is `true` or it is JSON-schema according to which the data is valid => go to step 3. - 2. `then` property is `false` or it is JSON-schema according to which the data is invalid => the validation of `switch` FAILS. -3. `continue`: - 1. `continue` property is `true` => go to the NEXT clause, if this was the last clause the validation of `switch` SUCCEEDS. - 2. `continue` property is `false` or absent => validation of `switch` SUCCEEDS. +If the data is invalid against the sub-schema in `if` keyword, then the validation result is equal to the result of data validation against the sub-schema in `else` keyword (if `else` is absent, the validation succeeds). __Examples__ @@ -861,29 +811,24 @@ __Examples__ ```json { - "switch": [ - { - "if": { "properties": { "power": { "minimum": 9000 } } }, - "then": { "required": [ "disbelief" ] }, - "continue": true - }, - { "then": { "required": [ "confidence" ] } } - ] + "if": { "properties": { "power": { "minimum": 9000 } } }, + "then": { "required": [ "disbelief" ] }, + "else": { "required": [ "confidence" ] } } ``` _valid_: - - `{ "power": 9000, "disbelief": true, "confidence": true }` - - `{ "confidence": true }` + - `{ "power": 10000, "disbelief": true }` + - `{}` - `{ "power": 1000, "confidence": true }` + - any non-object _invalid_: - - `{ "power": 9000 }` (`disbelief` & `confidence` are required) - - `{ "power": 9000, "disbelief": true }` (`confidence` is always required) - - `{ "power": 1000 }` - - `{}` + - `{ "power": 10000 }` (`disbelief` is required) + - `{ "power": 10000, "confidence": true }` (`disbelief` is required) + - `{ "power": 1000 }` (`confidence` is required) 2. _schema_: @@ -891,13 +836,14 @@ __Examples__ ```json { "type": "integer", - "switch": [ - { "if": { "not": { "minimum": 1 } }, "then": false }, - { "if": { "maximum": 10 }, "then": true }, - { "if": { "maximum": 100 }, "then": { "multipleOf": 10 } }, - { "if": { "maximum": 1000 }, "then": { "multipleOf": 100 } }, - { "then": false } - ] + "minimum": 1, + "maximum": 1000, + "if": { "minimum": 100 }, + "then": { "multipleOf": 100 }, + "else": { + "if": { "minimum": 10 }, + "then": { "multipleOf": 10 } + } } ``` diff --git a/LICENSE b/LICENSE index 810539685..96ee71998 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Evgeny Poberezkin +Copyright (c) 2015-2017 Evgeny Poberezkin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 62a52516a..5e502db93 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,76 @@ -Ajv logo +Ajv logo # Ajv: Another JSON Schema Validator -The fastest JSON Schema validator for Node.js and browser with draft 6 support. +The fastest JSON Schema validator for Node.js and browser. Supports draft-04/06/07. - -[![Build Status](https://travis-ci.org/epoberezkin/ajv.svg?branch=master)](https://travis-ci.org/epoberezkin/ajv) -[![npm version](https://badge.fury.io/js/ajv.svg)](https://www.npmjs.com/package/ajv) +[![Build Status](https://travis-ci.org/ajv-validator/ajv.svg?branch=master)](https://travis-ci.org/ajv-validator/ajv) +[![npm](https://img.shields.io/npm/v/ajv.svg)](https://www.npmjs.com/package/ajv) [![npm downloads](https://img.shields.io/npm/dm/ajv.svg)](https://www.npmjs.com/package/ajv) -[![Code Climate](https://codeclimate.com/github/epoberezkin/ajv/badges/gpa.svg)](https://codeclimate.com/github/epoberezkin/ajv) -[![Coverage Status](https://coveralls.io/repos/epoberezkin/ajv/badge.svg?branch=master&service=github)](https://coveralls.io/github/epoberezkin/ajv?branch=master) -[![Greenkeeper badge](https://badges.greenkeeper.io/epoberezkin/ajv.svg)](https://greenkeeper.io/) +[![Coverage Status](https://coveralls.io/repos/github/ajv-validator/ajv/badge.svg?branch=master)](https://coveralls.io/github/ajv-validator/ajv?branch=master) [![Gitter](https://img.shields.io/gitter/room/ajv-validator/ajv.svg)](https://gitter.im/ajv-validator/ajv) +[![GitHub Sponsors](https://img.shields.io/badge/$-sponsors-brightgreen)](https://github.com/sponsors/epoberezkin) + + +## Please [sponsor Ajv development](https://github.com/sponsors/epoberezkin) + +I will get straight to the point - I need your support to ensure that the development of Ajv continues. + +I have developed Ajv for 5 years in my free time, but it is not sustainable. I'd appreciate if you consider supporting its further development with donations: +- [GitHub sponsors page](https://github.com/sponsors/epoberezkin) (GitHub will match it) +- [Ajv Open Collective️](https://opencollective.com/ajv) + +There are many small and large improvements that are long due, including the support of the next versions of JSON Schema specification, improving website and documentation, and making Ajv more modular and maintainable to address its limitations - what Ajv needs to evolve is much more than what I can contribute in my free time. + +I would also really appreciate any advice you could give on how to raise funds for Ajv development - whether some suitable open-source fund I could apply to or some sponsor I should approach. + +Since 2015 Ajv has become widely used, thanks to your help and contributions: + +- **90** contributors 🏗 +- **5,000** dependent npm packages ⚙️ +- **7,000** github stars, from GitHub users [all over the world](https://www.google.com/maps/d/u/0/viewer?mid=1MGRV8ciFUGIbO1l0EKFWNJGYE7iSkDxP&ll=-3.81666561775622e-14%2C4.821737100000007&z=2) ⭐️ +- **5,000,000** dependent repositories on GitHub 🚀 +- **120,000,000** npm downloads per month! 💯 + +I believe it would benefit all Ajv users to help put together the fund that will be used for its further development - it would allow to bring some additional maintainers to the project. + +Thank you + + +#### Open Collective sponsors + + + + + + + + + + + + + + +## Using version 6 -## Using version 5 +[JSON Schema draft-07](http://json-schema.org/latest/json-schema-validation.html) is published. -[JSON Schema draft-06](https://trac.tools.ietf.org/html/draft-wright-json-schema-validation-01) is published. +[Ajv version 6.0.0](https://github.com/ajv-validator/ajv/releases/tag/v6.0.0) that supports draft-07 is released. It may require either migrating your schemas or updating your code (to continue using draft-04 and v5 schemas, draft-06 schemas will be supported without changes). -[Ajv version 5.0.0](https://github.com/epoberezkin/ajv/releases/tag/5.0.0) that supports draft-06 is released. It may require either migrating your schemas or updating your code (to continue using draft-04 and v5 schemas). +__Please note__: To use Ajv with draft-06 schemas you need to explicitly add the meta-schema to the validator instance: -__Please note__: To use Ajv with draft-04 schemas you need to explicitly add meta-schema to the validator instance: +```javascript +ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); +``` + +To use Ajv with draft-04 schemas in addition to explicitly adding meta-schema you also need to use option schemaId: ```javascript +var ajv = new Ajv({schemaId: 'id'}); +// If you want to use both draft-04 and draft-06/07 schemas: +// var ajv = new Ajv({schemaId: 'auto'}); ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); ``` @@ -32,11 +80,13 @@ ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); - [Performance](#performance) - [Features](#features) - [Getting started](#getting-started) -- [Frequently Asked Questions](https://github.com/epoberezkin/ajv/blob/master/FAQ.md) +- [Frequently Asked Questions](https://github.com/ajv-validator/ajv/blob/master/FAQ.md) - [Using in browser](#using-in-browser) + - [Ajv and Content Security Policies (CSP)](#ajv-and-content-security-policies-csp) - [Command line interface](#command-line-interface) - Validation - [Keywords](#validation-keywords) + - [Annotation keywords](#annotation-keywords) - [Formats](#formats) - [Combining schemas with $ref](#ref) - [$data reference](#data-reference) @@ -44,6 +94,12 @@ ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); - [Defining custom keywords](#defining-custom-keywords) - [Asynchronous schema compilation](#asynchronous-schema-compilation) - [Asynchronous validation](#asynchronous-validation) +- [Security considerations](#security-considerations) + - [Security contact](#security-contact) + - [Untrusted schemas](#untrusted-schemas) + - [Circular references in objects](#circular-references-in-javascript-objects) + - [Trusted schemas](#security-risks-of-trusted-schemas) + - [ReDoS attack](#redos-attack) - Modifying data during validation - [Filtering data](#filtering-data) - [Assigning defaults](#assigning-defaults) @@ -52,14 +108,16 @@ ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); - [Methods](#api) - [Options](#options) - [Validation errors](#validation-errors) +- [Plugins](#plugins) - [Related packages](#related-packages) -- [Packages using Ajv](#some-packages-using-ajv) -- [Tests, Contributing, History, License](#tests) +- [Some packages using Ajv](#some-packages-using-ajv) +- [Tests, Contributing, Changes history](#tests) +- [Support, Code of conduct, License](#open-source-software-support) ## Performance -Ajv generates code using [doT templates](https://github.com/olado/doT) to turn JSON schemas into super-fast validation functions that are efficient for v8 optimization. +Ajv generates code using [doT templates](https://github.com/olado/doT) to turn JSON Schemas into super-fast validation functions that are efficient for v8 optimization. Currently Ajv is the fastest and the most standard compliant validator according to these benchmarks: @@ -71,30 +129,30 @@ Currently Ajv is the fastest and the most standard compliant validator according Performance of different validators by [json-schema-benchmark](https://github.com/ebdrup/json-schema-benchmark): -[![performance](https://chart.googleapis.com/chart?chxt=x,y&cht=bhs&chco=76A4FB&chls=2.0&chbh=32,4,1&chs=600x416&chxl=-1:%7Cajv%7Cis-my-json-valid%7Cjsen%7Cschemasaurus%7Cthemis%7Cz-schema%7Cjsck%7Cjsonschema%7Cskeemas%7Ctv4%7Cjayschema&chd=t:100,68,61,22.8,17.6,6.6,2.7,0.9,0.7,0.4,0.1)](https://github.com/ebdrup/json-schema-benchmark/blob/master/README.md#performance) +[![performance](https://chart.googleapis.com/chart?chxt=x,y&cht=bhs&chco=76A4FB&chls=2.0&chbh=32,4,1&chs=600x416&chxl=-1:|djv|ajv|json-schema-validator-generator|jsen|is-my-json-valid|themis|z-schema|jsck|skeemas|json-schema-library|tv4&chd=t:100,98,72.1,66.8,50.1,15.1,6.1,3.8,1.2,0.7,0.2)](https://github.com/ebdrup/json-schema-benchmark/blob/master/README.md#performance) ## Features -- Ajv implements full JSON Schema [draft 6](http://json-schema.org/) and draft 4 standards: - - all validation keywords (see [JSON Schema validation keywords](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md)) +- Ajv implements full JSON Schema [draft-06/07](http://json-schema.org/) and draft-04 standards: + - all validation keywords (see [JSON Schema validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md)) - full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available) - support of circular references between schemas - correct string lengths for strings with unicode pairs (can be turned off) - - [formats](#formats) defined by JSON Schema draft 4 standard and custom formats (can be turned off) + - [formats](#formats) defined by JSON Schema draft-07 standard and custom formats (can be turned off) - [validates schemas against meta-schema](#api-validateschema) -- supports [browsers](#using-in-browser) and Node.js 0.10-8.x +- supports [browsers](#using-in-browser) and Node.js 0.10-14.x - [asynchronous loading](#asynchronous-schema-compilation) of referenced schemas during compilation - "All errors" validation mode with [option allErrors](#options) - [error messages with parameters](#validation-errors) describing error reasons to allow creating custom error messages -- i18n error messages support with [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) package +- i18n error messages support with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) package - [filtering data](#filtering-data) from additional properties - [assigning defaults](#assigning-defaults) to missing properties and items - [coercing data](#coercing-data-types) to the types specified in `type` keywords - [custom keywords](#defining-custom-keywords) -- draft-6 keywords `const`, `contains` and `propertyNames` -- draft-6 boolean schemas (`true`/`false` as a schema to always pass/fail). -- keywords `switch`, `patternRequired`, `formatMaximum` / `formatMinimum` and `formatExclusiveMaximum` / `formatExclusiveMinimum` from [JSON-schema extension proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) with [ajv-keywords](https://github.com/epoberezkin/ajv-keywords) package +- draft-06/07 keywords `const`, `contains`, `propertyNames` and `if/then/else` +- draft-06 boolean schemas (`true`/`false` as a schema to always pass/fail). +- keywords `switch`, `patternRequired`, `formatMaximum` / `formatMinimum` and `formatExclusiveMaximum` / `formatExclusiveMinimum` from [JSON Schema extension proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) with [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package - [$data reference](#data-reference) to use values from the validated data as values for the schema keywords - [asynchronous validation](#asynchronous-validation) of custom formats and keywords @@ -116,7 +174,11 @@ Try it in the Node.js REPL: https://tonicdev.com/npm/ajv The fastest validation call: ```javascript +// Node.js require: var Ajv = require('ajv'); +// or ESM/TypeScript import +import Ajv from 'ajv'; + var ajv = new Ajv(); // options can be passed, e.g. {allErrors: true} var validate = ajv.compile(schema); var valid = validate(data); @@ -136,20 +198,24 @@ or ```javascript // ... -ajv.addSchema(schema, 'mySchema'); -var valid = ajv.validate('mySchema', data); +var valid = ajv.addSchema(schema, 'mySchema') + .validate('mySchema', data); if (!valid) console.log(ajv.errorsText()); // ... ``` See [API](#api) and [Options](#options) for more details. -Ajv compiles schemas to functions and caches them in all cases (using schema serialized with [json-stable-stringify](https://github.com/substack/json-stable-stringify) or a custom function as a key), so that the next time the same schema is used (not necessarily the same object instance) it won't be compiled again. +Ajv compiles schemas to functions and caches them in all cases (using schema serialized with [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) or a custom function as a key), so that the next time the same schema is used (not necessarily the same object instance) it won't be compiled again. The best performance is achieved when using compiled functions returned by `compile` or `getSchema` methods (there is no additional function call). __Please note__: every time a validation function or `ajv.validate` are called `errors` property is overwritten. You need to copy `errors` array reference to another variable if you want to use it later (e.g., in the callback). See [Validation errors](#validation-errors) +__Note for TypeScript users__: `ajv` provides its own TypeScript declarations +out of the box, so you don't need to install the deprecated `@types/ajv` +module. + ## Using in browser @@ -170,56 +236,85 @@ Ajv is tested with these browsers: [![Sauce Test Status](https://saucelabs.com/browser-matrix/epoberezkin.svg)](https://saucelabs.com/u/epoberezkin) -__Please note__: some frameworks, e.g. Dojo, may redefine global require in such way that is not compatible with CommonJS module format. In such case Ajv bundle has to be loaded before the framework and then you can use global Ajv (see issue [#234](https://github.com/epoberezkin/ajv/issues/234)). +__Please note__: some frameworks, e.g. Dojo, may redefine global require in such way that is not compatible with CommonJS module format. In such case Ajv bundle has to be loaded before the framework and then you can use global Ajv (see issue [#234](https://github.com/ajv-validator/ajv/issues/234)). + + +### Ajv and Content Security Policies (CSP) + +If you're using Ajv to compile a schema (the typical use) in a browser document that is loaded with a Content Security Policy (CSP), that policy will require a `script-src` directive that includes the value `'unsafe-eval'`. +:warning: NOTE, however, that `unsafe-eval` is NOT recommended in a secure CSP[[1]](https://developer.chrome.com/extensions/contentSecurityPolicy#relaxing-eval), as it has the potential to open the document to cross-site scripting (XSS) attacks. + +In order to make use of Ajv without easing your CSP, you can [pre-compile a schema using the CLI](https://github.com/ajv-validator/ajv-cli#compile-schemas). This will transpile the schema JSON into a JavaScript file that exports a `validate` function that works simlarly to a schema compiled at runtime. + +Note that pre-compilation of schemas is performed using [ajv-pack](https://github.com/ajv-validator/ajv-pack) and there are [some limitations to the schema features it can compile](https://github.com/ajv-validator/ajv-pack#limitations). A successfully pre-compiled schema is equivalent to the same schema compiled at runtime. ## Command line interface -CLI is available as a separate npm package [ajv-cli](https://github.com/jessedc/ajv-cli). It supports: +CLI is available as a separate npm package [ajv-cli](https://github.com/ajv-validator/ajv-cli). It supports: -- compiling JSON-schemas to test their validity -- BETA: generating standalone module exporting a validation function to be used without Ajv (using [ajv-pack](https://github.com/epoberezkin/ajv-pack)) -- migrate schemas to draft-06 (using [json-schema-migrate](https://github.com/epoberezkin/json-schema-migrate)) -- validating data file(s) against JSON-schema -- testing expected validity of data against JSON-schema +- compiling JSON Schemas to test their validity +- BETA: generating standalone module exporting a validation function to be used without Ajv (using [ajv-pack](https://github.com/ajv-validator/ajv-pack)) +- migrate schemas to draft-07 (using [json-schema-migrate](https://github.com/epoberezkin/json-schema-migrate)) +- validating data file(s) against JSON Schema +- testing expected validity of data against JSON Schema - referenced schemas - custom meta-schemas -- files in JSON and JavaScript format +- files in JSON, JSON5, YAML, and JavaScript format - all Ajv options - reporting changes in data after validation in [JSON-patch](https://tools.ietf.org/html/rfc6902) format ## Validation keywords -Ajv supports all validation keywords from draft 4 of JSON-schema standard: +Ajv supports all validation keywords from draft-07 of JSON Schema standard: + +- [type](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#type) +- [for numbers](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-numbers) - maximum, minimum, exclusiveMaximum, exclusiveMinimum, multipleOf +- [for strings](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-strings) - maxLength, minLength, pattern, format +- [for arrays](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-arrays) - maxItems, minItems, uniqueItems, items, additionalItems, [contains](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#contains) +- [for objects](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-objects) - maxProperties, minProperties, required, properties, patternProperties, additionalProperties, dependencies, [propertyNames](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#propertynames) +- [for all types](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-all-types) - enum, [const](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#const) +- [compound keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#compound-keywords) - not, oneOf, anyOf, allOf, [if/then/else](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#ifthenelse) + +With [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package Ajv also supports validation keywords from [JSON Schema extension proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) for JSON Schema standard: -- [type](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#type) -- [for numbers](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#keywords-for-numbers) - maximum, minimum, exclusiveMaximum, exclusiveMinimum, multipleOf -- [for strings](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#keywords-for-strings) - maxLength, minLength, pattern, format -- [for arrays](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#keywords-for-arrays) - maxItems, minItems, uniqueItems, items, additionalItems, [contains](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#contains) -- [for objects](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#keywords-for-objects) - maxProperties, minProperties, required, properties, patternProperties, additionalProperties, dependencies, [propertyNames](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#propertynames) -- [for all types](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#keywords-for-all-types) - enum, [const](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#const) -- [compound keywords](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#compound-keywords) - not, oneOf, anyOf, allOf +- [patternRequired](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#patternrequired-proposed) - like `required` but with patterns that some property should match. +- [formatMaximum, formatMinimum, formatExclusiveMaximum, formatExclusiveMinimum](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#formatmaximum--formatminimum-and-exclusiveformatmaximum--exclusiveformatminimum-proposed) - setting limits for date, time, etc. -With [ajv-keywords](https://github.com/epoberezkin/ajv-keywords) package Ajv also supports validation keywords from [JSON Schema extension proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) for JSON-schema standard: +See [JSON Schema validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md) for more details. -- [switch](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#switch-proposed) - conditional validation with a sequence of if/then clauses -- [patternRequired](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#patternrequired-proposed) - like `required` but with patterns that some property should match. -- [formatMaximum, formatMinimum, formatExclusiveMaximum, formatExclusiveMinimum](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md#formatmaximum--formatminimum-and-exclusiveformatmaximum--exclusiveformatminimum-proposed) - setting limits for date, time, etc. -See [JSON Schema validation keywords](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md) for more details. +## Annotation keywords + +JSON Schema specification defines several annotation keywords that describe schema itself but do not perform any validation. + +- `title` and `description`: information about the data represented by that schema +- `$comment` (NEW in draft-07): information for developers. With option `$comment` Ajv logs or passes the comment string to the user-supplied function. See [Options](#options). +- `default`: a default value of the data instance, see [Assigning defaults](#assigning-defaults). +- `examples` (NEW in draft-06): an array of data instances. Ajv does not check the validity of these instances against the schema. +- `readOnly` and `writeOnly` (NEW in draft-07): marks data-instance as read-only or write-only in relation to the source of the data (database, api, etc.). +- `contentEncoding`: [RFC 2045](https://tools.ietf.org/html/rfc2045#section-6.1 ), e.g., "base64". +- `contentMediaType`: [RFC 2046](https://tools.ietf.org/html/rfc2046), e.g., "image/png". + +__Please note__: Ajv does not implement validation of the keywords `examples`, `contentEncoding` and `contentMediaType` but it reserves them. If you want to create a plugin that implements some of them, it should remove these keywords from the instance. ## Formats -The following formats are supported for string validation with "format" keyword: +Ajv implements formats defined by JSON Schema specification and several other formats. It is recommended NOT to use "format" keyword implementations with untrusted data, as they use potentially unsafe regular expressions - see [ReDoS attack](#redos-attack). + +__Please note__: if you need to use "format" keyword to validate untrusted data, you MUST assess their suitability and safety for your validation scenarios. + +The following formats are implemented for string validation with "format" keyword: - _date_: full-date according to [RFC3339](http://tools.ietf.org/html/rfc3339#section-5.6). - _time_: time with optional time-zone. - _date-time_: date-time from the same source (time-zone is mandatory). `date`, `time` and `date-time` validate ranges in `full` mode and only regexp in `fast` mode (see [options](#options)). -- _uri_: full uri with optional protocol. -- _url_: [URL record](https://url.spec.whatwg.org/#concept-url). +- _uri_: full URI. +- _uri-reference_: URI reference, including full and relative URIs. - _uri-template_: URI template according to [RFC6570](https://tools.ietf.org/html/rfc6570) +- _url_ (deprecated): [URL record](https://url.spec.whatwg.org/#concept-url). - _email_: email address. - _hostname_: host name according to [RFC1034](http://tools.ietf.org/html/rfc1034#section-3.5). - _ipv4_: IP address v4. @@ -229,13 +324,15 @@ The following formats are supported for string validation with "format" keyword: - _json-pointer_: JSON-pointer according to [RFC6901](https://tools.ietf.org/html/rfc6901). - _relative-json-pointer_: relative JSON-pointer according to [this draft](http://tools.ietf.org/html/draft-luff-relative-json-pointer-00). -There are two modes of format validation: `fast` and `full`. This mode affects formats `date`, `time`, `date-time`, `uri`, `email`, and `hostname`. See [Options](#options) for details. +__Please note__: JSON Schema draft-07 also defines formats `iri`, `iri-reference`, `idn-hostname` and `idn-email` for URLs, hostnames and emails with international characters. Ajv does not implement these formats. If you create Ajv plugin that implements them please make a PR to mention this plugin here. + +There are two modes of format validation: `fast` and `full`. This mode affects formats `date`, `time`, `date-time`, `uri`, `uri-reference`, and `email`. See [Options](#options) for details. You can add additional formats and replace any of the formats above using [addFormat](#api-addformat) method. The option `unknownFormats` allows changing the default behaviour when an unknown format is encountered. In this case Ajv can either fail schema compilation (default) or ignore it (default in versions before 5.0.0). You also can whitelist specific format(s) to be ignored. See [Options](#options) for details. -You can find patterns used for format validation and the sources that were used in [formats.js](https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js). +You can find regular expressions used for format validation and the sources that were used in [formats.js](https://github.com/ajv-validator/ajv/blob/master/lib/compile/formats.js). ## Combining schemas with $ref @@ -274,8 +371,8 @@ or use `addSchema` method: ```javascript var ajv = new Ajv; -ajv.addSchema(defsSchema); -var validate = ajv.compile(schema); +var validate = ajv.addSchema(defsSchema) + .compile(schema); ``` See [Options](#options) and [addSchema](#api) method. @@ -292,7 +389,7 @@ __Please note__: ## $data reference -With `$data` option you can use values from the validated data as the values for the schema keywords. See [proposal](https://github.com/json-schema/json-schema/wiki/$data-(v5-proposal)) for more information about how it works. +With `$data` option you can use values from the validated data as the values for the schema keywords. See [proposal](https://github.com/json-schema-org/json-schema-spec/issues/51) for more information about how it works. `$data` reference is supported in the keywords: const, enum, format, maximum/minimum, exclusiveMaximum / exclusiveMinimum, maxLength / minLength, maxItems / minItems, maxProperties / minProperties, formatMaximum / formatMinimum, formatExclusiveMaximum / formatExclusiveMinimum, multipleOf, pattern, required, uniqueItems. @@ -344,7 +441,7 @@ var validData = { ## $merge and $patch keywords -With the package [ajv-merge-patch](https://github.com/epoberezkin/ajv-merge-patch) you can use the keywords `$merge` and `$patch` that allow extending JSON-schemas with patches using formats [JSON Merge Patch (RFC 7396)](https://tools.ietf.org/html/rfc7396) and [JSON Patch (RFC 6902)](https://tools.ietf.org/html/rfc6902). +With the package [ajv-merge-patch](https://github.com/ajv-validator/ajv-merge-patch) you can use the keywords `$merge` and `$patch` that allow extending JSON Schemas with patches using formats [JSON Merge Patch (RFC 7396)](https://tools.ietf.org/html/rfc7396) and [JSON Patch (RFC 6902)](https://tools.ietf.org/html/rfc6902). To add keywords `$merge` and `$patch` to Ajv instance use this code: @@ -403,7 +500,7 @@ The schemas above are equivalent to this schema: The properties `source` and `with` in the keywords `$merge` and `$patch` can use absolute or relative `$ref` to point to other schemas previously added to the Ajv instance or to the fragments of the current schema. -See the package [ajv-merge-patch](https://github.com/epoberezkin/ajv-merge-patch) for more information. +See the package [ajv-merge-patch](https://github.com/ajv-validator/ajv-merge-patch) for more information. ## Defining custom keywords @@ -418,7 +515,7 @@ The advantages of using custom keywords are: If a keyword is used only for side-effects and its validation result is pre-defined, use option `valid: true/false` in keyword definition to simplify both generated code (no error handling in case of `valid: true`) and your keyword functions (no need to return any validation result). -The concerns you have to be aware of when extending JSON-schema standard with custom keywords are the portability and understanding of your schemas. You will have to support these custom keywords on other platforms and to properly document these keywords so that everybody can understand them in your schemas. +The concerns you have to be aware of when extending JSON Schema standard with custom keywords are the portability and understanding of your schemas. You will have to support these custom keywords on other platforms and to properly document these keywords so that everybody can understand them in your schemas. You can define custom keywords with [addKeyword](#api-addkeyword) method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords. @@ -431,14 +528,17 @@ Ajv allows defining keywords with: Example. `range` and `exclusiveRange` keywords using compiled schema: ```javascript -ajv.addKeyword('range', { type: 'number', compile: function (sch, parentSchema) { - var min = sch[0]; - var max = sch[1]; +ajv.addKeyword('range', { + type: 'number', + compile: function (sch, parentSchema) { + var min = sch[0]; + var max = sch[1]; - return parentSchema.exclusiveRange === true - ? function (data) { return data > min && data < max; } - : function (data) { return data >= min && data <= max; } -} }); + return parentSchema.exclusiveRange === true + ? function (data) { return data > min && data < max; } + : function (data) { return data >= min && data <= max; } + } +}); var schema = { "range": [2, 4], "exclusiveRange": true }; var validate = ajv.compile(schema); @@ -448,9 +548,9 @@ console.log(validate(2)); // false console.log(validate(4)); // false ``` -Several custom keywords (typeof, instanceof, range and propertyNames) are defined in [ajv-keywords](https://github.com/epoberezkin/ajv-keywords) package - they can be used for your schemas and as a starting point for your own custom keywords. +Several custom keywords (typeof, instanceof, range and propertyNames) are defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package - they can be used for your schemas and as a starting point for your own custom keywords. -See [Defining custom keywords](https://github.com/epoberezkin/ajv/blob/master/CUSTOM.md) for more details. +See [Defining custom keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md) for more details. ## Asynchronous schema compilation @@ -489,17 +589,11 @@ If your schema uses asynchronous formats/keywords or refers to some schema that __Please note__: all asynchronous subschemas that are referenced from the current or other schemas should have `"$async": true` keyword as well, otherwise the schema compilation will fail. -Validation function for an asynchronous custom format/keyword should return a promise that resolves with `true` or `false` (or rejects with `new Ajv.ValidationError(errors)` if you want to return custom errors from the keyword function). Ajv compiles asynchronous schemas to either [es7 async functions](http://tc39.github.io/ecmascript-asyncawait/) that can optionally be transpiled with [nodent](https://github.com/MatAtBread/nodent) or with [regenerator](https://github.com/facebook/regenerator) or to [generator functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) that can be optionally transpiled with regenerator as well. You can also supply any other transpiler as a function. See [Options](#options). - -The compiled validation function has `$async: true` property (if the schema is asynchronous), so you can differentiate these functions if you are using both synchronous and asynchronous schemas. - -If you are using generators, the compiled validation function can be either wrapped with [co](https://github.com/tj/co) (default) or returned as generator function, that can be used directly, e.g. in [koa](http://koajs.com/) 1.0. `co` is a small library, it is included in Ajv (both as npm dependency and in the browser bundle). - -Async functions are currently supported in Chrome 55, Firefox 52, Node.js 7 (with --harmony-async-await) and MS Edge 13 (with flag). +Validation function for an asynchronous custom format/keyword should return a promise that resolves with `true` or `false` (or rejects with `new Ajv.ValidationError(errors)` if you want to return custom errors from the keyword function). -Generator functions are currently supported in Chrome, Firefox and Node.js. +Ajv compiles asynchronous schemas to [es7 async functions](http://tc39.github.io/ecmascript-asyncawait/) that can optionally be transpiled with [nodent](https://github.com/MatAtBread/nodent). Async functions are supported in Node.js 7+ and all modern browsers. You can also supply any other transpiler as a function via `processCode` option. See [Options](#options). -If you are using Ajv in other browsers or in older versions of Node.js you should use one of available transpiling options. All provided async modes use global Promise class. If your platform does not have Promise you should use a polyfill that defines it. +The compiled validation function has `$async: true` property (if the schema is asynchronous), so you can differentiate these functions if you are using both synchronous and asynchronous schemas. Validation result will be a promise that resolves with validated data or rejects with an exception `Ajv.ValidationError` that contains the array of validation errors in `errors` property. @@ -507,21 +601,8 @@ Validation result will be a promise that resolves with validated data or rejects Example: ```javascript -/** - * Default mode is non-transpiled generator function wrapped with `co`. - * Using package ajv-async (https://github.com/epoberezkin/ajv-async) - * you can auto-detect the best async mode. - * In this case, without "async" and "transpile" options - * (or with option {async: true}) - * Ajv will choose the first supported/installed option in this order: - * 1. native async function - * 2. native generator function wrapped with co - * 3. es7 async functions transpiled with nodent - * 4. es7 async functions transpiled with regenerator - */ - -var setupAsync = require('ajv-async'); -var ajv = setupAsync(new Ajv); +var ajv = new Ajv; +// require('ajv-async')(ajv); ajv.addKeyword('idExists', { async: true, @@ -568,66 +649,108 @@ validate({ userId: 1, postId: 19 }) ### Using transpilers with asynchronous validation functions. -To use a transpiler you should separately install it (or load its bundle in the browser). - -Ajv npm package includes minified browser bundles of regenerator and nodent in dist folder. +[ajv-async](https://github.com/ajv-validator/ajv-async) uses [nodent](https://github.com/MatAtBread/nodent) to transpile async functions. To use another transpiler you should separately install it (or load its bundle in the browser). #### Using nodent ```javascript -var setupAsync = require('ajv-async'); -var ajv = new Ajv({ /* async: 'es7', */ transpile: 'nodent' }); -setupAsync(ajv); +var ajv = new Ajv; +require('ajv-async')(ajv); +// in the browser if you want to load ajv-async bundle separately you can: +// window.ajvAsync(ajv); var validate = ajv.compile(schema); // transpiled es7 async function validate(data).then(successFunc).catch(errorFunc); ``` -`npm install nodent` or use `nodent.min.js` from dist folder of npm package. - -#### Using regenerator +#### Using other transpilers ```javascript -var setupAsync = require('ajv-async'); -var ajv = new Ajv({ /* async: 'es7', */ transpile: 'regenerator' }); -setupAsync(ajv); +var ajv = new Ajv({ processCode: transpileFunc }); var validate = ajv.compile(schema); // transpiled es7 async function validate(data).then(successFunc).catch(errorFunc); ``` -`npm install regenerator` or use `regenerator.min.js` from dist folder of npm package. +See [Options](#options). -#### Using other transpilers +## Security considerations + +JSON Schema, if properly used, can replace data sanitisation. It doesn't replace other API security considerations. It also introduces additional security aspects to consider. + + +##### Security contact + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. Please do NOT report security vulnerabilities via GitHub issues. + + +##### Untrusted schemas + +Ajv treats JSON schemas as trusted as your application code. This security model is based on the most common use case, when the schemas are static and bundled together with the application. + +If your schemas are received from untrusted sources (or generated from untrusted data) there are several scenarios you need to prevent: +- compiling schemas can cause stack overflow (if they are too deep) +- compiling schemas can be slow (e.g. [#557](https://github.com/ajv-validator/ajv/issues/557)) +- validating certain data can be slow + +It is difficult to predict all the scenarios, but at the very least it may help to limit the size of untrusted schemas (e.g. limit JSON string length) and also the maximum schema object depth (that can be high for relatively small JSON strings). You also may want to mitigate slow regular expressions in `pattern` and `patternProperties` keywords. + +Regardless the measures you take, using untrusted schemas increases security risks. + + +##### Circular references in JavaScript objects + +Ajv does not support schemas and validated data that have circular references in objects. See [issue #802](https://github.com/ajv-validator/ajv/issues/802). + +An attempt to compile such schemas or validate such data would cause stack overflow (or will not complete in case of asynchronous validation). Depending on the parser you use, untrusted data can lead to circular references. + + +##### Security risks of trusted schemas + +Some keywords in JSON Schemas can lead to very slow validation for certain data. These keywords include (but may be not limited to): + +- `pattern` and `format` for large strings - in some cases using `maxLength` can help mitigate it, but certain regular expressions can lead to exponential validation time even with relatively short strings (see [ReDoS attack](#redos-attack)). +- `patternProperties` for large property names - use `propertyNames` to mitigate, but some regular expressions can have exponential evaluation time as well. +- `uniqueItems` for large non-scalar arrays - use `maxItems` to mitigate + +__Please note__: The suggestions above to prevent slow validation would only work if you do NOT use `allErrors: true` in production code (using it would continue validation after validation errors). + +You can validate your JSON schemas against [this meta-schema](https://github.com/ajv-validator/ajv/blob/master/lib/refs/json-schema-secure.json) to check that these recommendations are followed: ```javascript -var ajv = new Ajv({ async: 'es7', processCode: transpileFunc }); -var validate = ajv.compile(schema); // transpiled es7 async function -validate(data).then(successFunc).catch(errorFunc); +const isSchemaSecure = ajv.compile(require('ajv/lib/refs/json-schema-secure.json')); + +const schema1 = {format: 'email'}; +isSchemaSecure(schema1); // false + +const schema2 = {format: 'email', maxLength: MAX_LENGTH}; +isSchemaSecure(schema2); // true ``` -See [Options](#options). +__Please note__: following all these recommendation is not a guarantee that validation of untrusted data is safe - it can still lead to some undesirable results. -#### Comparison of async modes +##### Content Security Policies (CSP) +See [Ajv and Content Security Policies (CSP)](#ajv-and-content-security-policies-csp) -|mode|transpile
speed*|run-time
speed*|bundle
size| -|---|:-:|:-:|:-:| -|es7 async
(native)|-|0.75|-| -|generators
(native)|-|1.0|-| -|es7.nodent|1.35|1.1|215Kb| -|es7.regenerator|1.0|2.7|1109Kb| -|regenerator|1.0|3.2|1109Kb| -\* Relative performance in Node.js 7.x — smaller is better. +## ReDoS attack -[nodent](https://github.com/MatAtBread/nodent) has several advantages: +Certain regular expressions can lead to the exponential evaluation time even with relatively short strings. -- much smaller browser bundle than regenerator -- almost the same performance of generated code as native generators in Node.js and the latest Chrome -- much better performance than native generators in other browsers -- works in IE 9 (regenerator does not) +Please assess the regular expressions you use in the schemas on their vulnerability to this attack - see [safe-regex](https://github.com/substack/safe-regex), for example. + +__Please note__: some formats that Ajv implements use [regular expressions](https://github.com/ajv-validator/ajv/blob/master/lib/compile/formats.js) that can be vulnerable to ReDoS attack, so if you use Ajv to validate data from untrusted sources __it is strongly recommended__ to consider the following: + +- making assessment of "format" implementations in Ajv. +- using `format: 'fast'` option that simplifies some of the regular expressions (although it does not guarantee that they are safe). +- replacing format implementations provided by Ajv with your own implementations of "format" keyword that either uses different regular expressions or another approach to format validation. Please see [addFormat](#api-addformat) method. +- disabling format validation by ignoring "format" keyword with option `format: false` + +Whatever mitigation you choose, please assume all formats provided by Ajv as potentially unsafe and make your own assessment of their suitability for your validation scenarios. ## Filtering data @@ -700,7 +823,7 @@ The intention of the schema above is to allow objects with either the string pro With the option `removeAdditional: true` the validation will pass for the object `{ "foo": "abc"}` but will fail for the object `{"bar": 1}`. It happens because while the first subschema in `oneOf` is validated, the property `bar` is removed because it is an additional property according to the standard (because it is not included in `properties` keyword in the same schema). -While this behaviour is unexpected (issues [#129](https://github.com/epoberezkin/ajv/issues/129), [#134](https://github.com/epoberezkin/ajv/issues/134)), it is correct. To have the expected behaviour (both objects are allowed and additional properties are removed) the schema has to be refactored in this way: +While this behaviour is unexpected (issues [#129](https://github.com/ajv-validator/ajv/issues/129), [#134](https://github.com/ajv-validator/ajv/issues/134)), it is correct. To have the expected behaviour (both objects are allowed and additional properties are removed) the schema has to be refactored in this way: ```json { @@ -724,13 +847,11 @@ The schema above is also more efficient - it will compile into a faster function With [option `useDefaults`](#options) Ajv will assign values from `default` keyword in the schemas of `properties` and `items` (when it is the array of schemas) to the missing properties and items. -This option modifies original data. +With the option value `"empty"` properties and items equal to `null` or `""` (empty string) will be considered missing and assigned defaults. -__Please note__: by default the default value is inserted in the generated validation code as a literal (starting from v4.0), so the value inserted in the data will be the deep clone of the default in the schema. - -If you need to insert the default value in the data by reference pass the option `useDefaults: "shared"`. +This option modifies original data. -Inserting defaults by reference can be faster (in case you have an object in `default`) and it allows to have dynamic values in defaults, e.g. timestamp, without recompiling the schema. The side effect is that modifying the default value in any validated data instance will change the default in the schema and in other validated data instances. See example 3 below. +__Please note__: the default value is inserted in the generated validation code as a literal, so the value inserted in the data will be the deep clone of the default in the schema. Example 1 (`default` in `properties`): @@ -773,39 +894,15 @@ console.log(validate(data)); // true console.log(data); // [ 1, "foo" ] ``` -Example 3 (inserting "defaults" by reference): - -```javascript -var ajv = new Ajv({ useDefaults: 'shared' }); - -var schema = { - properties: { - foo: { - default: { bar: 1 } - } - } -} - -var validate = ajv.compile(schema); - -var data = {}; -console.log(validate(data)); // true -console.log(data); // { foo: { bar: 1 } } - -data.foo.bar = 2; - -var data2 = {}; -console.log(validate(data2)); // true -console.log(data2); // { foo: { bar: 2 } } -``` - `default` keywords in other cases are ignored: - not in `properties` or `items` subschemas -- in schemas inside `anyOf`, `oneOf` and `not` (see [#42](https://github.com/epoberezkin/ajv/issues/42)) +- in schemas inside `anyOf`, `oneOf` and `not` (see [#42](https://github.com/ajv-validator/ajv/issues/42)) - in `if` subschema of `switch` keyword - in schemas generated by custom macro keywords +The [`strictDefaults` option](#options) customizes Ajv's behavior for the defaults that Ajv ignores (`true` raises an error, and `"log"` outputs a warning). + ## Coercing data types @@ -858,7 +955,7 @@ console.log(data); // { "foo": [1], "bar": false } The coercion rules, as you can see from the example, are different from JavaScript both to validate user input as expected and to have the coercion reversible (to correctly validate cases where different types are defined in subschemas of "anyOf" and other compound keywords). -See [Coercion rules](https://github.com/epoberezkin/ajv/blob/master/COERCION.md) for details. +See [Coercion rules](https://github.com/ajv-validator/ajv/blob/master/COERCION.md) for details. ## API @@ -872,9 +969,9 @@ Create Ajv instance. Generate validating function and cache the compiled schema for future use. -Validating function returns boolean and has properties `errors` with the errors from the last validation (`null` if there were no errors) and `schema` with the reference to the original schema. +Validating function returns a boolean value. This function has properties `errors` and `schema`. Errors encountered during the last validation are assigned to `errors` property (it is assigned `null` if there was no errors). `schema` property contains the reference to the original schema. -Unless the option `validateSchema` is false, the schema will be validated against meta-schema and if schema is invalid the error will be thrown. See [options](#options). +The schema passed to this method will be validated against meta-schema unless `validateSchema` option is false. If schema is invalid, an error will be thrown. See [options](#options). ##### .compileAsync(Object schema [, Boolean meta] [, Function callback]) -> Promise @@ -905,7 +1002,7 @@ __Please note__: every time this method is called the errors are overwritten so If the schema is asynchronous (has `$async` keyword on the top level) this method returns a Promise. See [Asynchronous validation](#asynchronous-validation). -##### .addSchema(Array<Object>|Object schema [, String key]) +##### .addSchema(Array<Object>|Object schema [, String key]) -> Ajv Add schema(s) to validator instance. This method does not compile schemas (but it still validates them). Because of that dependencies can be added in any order and circular dependencies are supported. It also prevents unnecessary compilation of schemas that are containers for other schemas but not used as a whole. @@ -920,12 +1017,18 @@ Although `addSchema` does not compile schemas, explicit compilation is not requi By default the schema is validated against meta-schema before it is added, and if the schema does not pass validation the exception is thrown. This behaviour is controlled by `validateSchema` option. +__Please note__: Ajv uses the [method chaining syntax](https://en.wikipedia.org/wiki/Method_chaining) for all methods with the prefix `add*` and `remove*`. +This allows you to do nice things like the following. + +```javascript +var validate = new Ajv().addSchema(schema).addFormat(name, regex).getSchema(uri); +``` -##### .addMetaSchema(Array<Object>|Object schema [, String key]) +##### .addMetaSchema(Array<Object>|Object schema [, String key]) -> Ajv Adds meta schema(s) that can be used to validate other schemas. That function should be used instead of `addSchema` because there may be instance options that would compile a meta schema incorrectly (at the moment it is `removeAdditional` option). -There is no need to explicitly add draft 6 meta schema (http://json-schema.org/draft-06/schema and http://json-schema.org/schema) - it is added by default, unless option `meta` is set to `false`. You only need to use it if you have a changed meta-schema that you want to use to validate your schemas. See `validateSchema`. +There is no need to explicitly add draft-07 meta schema (http://json-schema.org/draft-07/schema) - it is added by default, unless option `meta` is set to `false`. You only need to use it if you have a changed meta-schema that you want to use to validate your schemas. See `validateSchema`. ##### .validateSchema(Object schema) -> Boolean @@ -946,7 +1049,7 @@ Errors will be available at `ajv.errors`. Retrieve compiled schema previously added with `addSchema` by the key passed to `addSchema` or by its full reference (id). The returned validating function has `schema` property with the reference to the original schema. -##### .removeSchema([Object schema|String key|String ref|RegExp pattern]) +##### .removeSchema([Object schema|String key|String ref|RegExp pattern]) -> Ajv Remove added/cached schema. Even if schema is referenced by other schemas it can be safely removed as dependent schemas have local references. @@ -959,7 +1062,7 @@ Schema can be removed using: If no parameter is passed all schemas but meta-schemas will be removed and the cache will be cleared. -##### .addFormat(String name, String|RegExp|Function|Object format) +##### .addFormat(String name, String|RegExp|Function|Object format) -> Ajv Add custom format to validate strings or numbers. It can also be used to replace pre-defined formats for Ajv instance. @@ -970,25 +1073,25 @@ Function should return validation result as `true` or `false`. If object is passed it should have properties `validate`, `compare` and `async`: - _validate_: a string, RegExp or a function as described above. -- _compare_: an optional comparison function that accepts two strings and compares them according to the format meaning. This function is used with keywords `formatMaximum`/`formatMinimum` (defined in [ajv-keywords](https://github.com/epoberezkin/ajv-keywords) package). It should return `1` if the first value is bigger than the second value, `-1` if it is smaller and `0` if it is equal. +- _compare_: an optional comparison function that accepts two strings and compares them according to the format meaning. This function is used with keywords `formatMaximum`/`formatMinimum` (defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package). It should return `1` if the first value is bigger than the second value, `-1` if it is smaller and `0` if it is equal. - _async_: an optional `true` value if `validate` is an asynchronous function; in this case it should return a promise that resolves with a value `true` or `false`. -- _type_: an optional type of data that the format applies to. It can be `"string"` (default) or `"number"` (see https://github.com/epoberezkin/ajv/issues/291#issuecomment-259923858). If the type of data is different, the validation will pass. +- _type_: an optional type of data that the format applies to. It can be `"string"` (default) or `"number"` (see https://github.com/ajv-validator/ajv/issues/291#issuecomment-259923858). If the type of data is different, the validation will pass. Custom formats can be also added via `formats` option. -##### .addKeyword(String keyword, Object definition) +##### .addKeyword(String keyword, Object definition) -> Ajv Add custom validation keyword to Ajv instance. -Keyword should be different from all standard JSON schema keywords and different from previously defined keywords. There is no way to redefine keywords or to remove keyword definition from the instance. +Keyword should be different from all standard JSON Schema keywords and different from previously defined keywords. There is no way to redefine keywords or to remove keyword definition from the instance. Keyword must start with a letter, `_` or `$`, and may continue with letters, numbers, `_`, `$`, or `-`. It is recommended to use an application-specific prefix for keywords to avoid current and future name collisions. Example Keywords: - `"xyz-example"`: valid, and uses prefix for the xyz project to avoid name collisions. -- `"example"`: valid, but not recommended as it could collide with future versions of JSON schema etc. +- `"example"`: valid, but not recommended as it could collide with future versions of JSON Schema etc. - `"3-example"`: invalid as numbers are not allowed to be the first character in a keyword Keyword definition is an object with the following properties: @@ -1000,11 +1103,13 @@ Keyword definition is an object with the following properties: - _inline_: compiling function that returns code (as string) - _schema_: an optional `false` value used with "validate" keyword to not pass schema - _metaSchema_: an optional meta-schema for keyword schema +- _dependencies_: an optional list of properties that must be present in the parent schema - it will be checked during schema compilation - _modifying_: `true` MUST be passed if keyword modifies data +- _statements_: `true` can be passed in case inline keyword generates statements (as opposed to expression) - _valid_: pass `true`/`false` to pre-define validation result, the result returned from validation function will be ignored. This option cannot be used with macro keywords. - _$data_: an optional `true` value to support [$data reference](#data-reference) as the value of custom keyword. The reference will be resolved at validation time. If the keyword has meta-schema it would be extended to allow $data and it will be used to validate the resolved value. Supporting $data reference requires that keyword has validating function (as the only option or in addition to compile, macro or inline function). - _async_: an optional `true` value if the validation function is asynchronous (whether it is compiled or passed in _validate_ property); in this case it should return a promise that resolves with a value `true` or `false`. This option is ignored in case of "macro" and "inline" keywords. -- _errors_: an optional boolean indicating whether keyword returns errors. If this property is not set Ajv will determine if the errors were set in case of failed validation. +- _errors_: an optional boolean or string `"full"` indicating whether keyword returns errors. If this property is not set Ajv will determine if the errors were set in case of failed validation. _compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time. _validate_ can be used separately or in addition to them to support $data reference. @@ -1018,7 +1123,7 @@ See [Defining custom keywords](#defining-custom-keywords) for more details. Returns custom keyword definition, `true` for pre-defined keywords and `false` if the keyword is unknown. -##### .removeKeyword(String keyword) +##### .removeKeyword(String keyword) -> Ajv Removes custom or pre-defined keyword so you can redefine them. @@ -1044,15 +1149,18 @@ Defaults: $data: false, allErrors: false, verbose: false, + $comment: false, // NEW in Ajv version 6.0 jsonPointers: false, uniqueItems: true, unicode: true, + nullable: false, format: 'fast', formats: {}, unknownFormats: true, schemas: {}, + logger: undefined, // referenced schema options: - schemaId: undefined // recommended '$id' + schemaId: '$id', missingRefs: true, extendRefs: 'ignore', // recommended 'fail' loadSchema: undefined, // function(uri: string): Promise {} @@ -1060,8 +1168,11 @@ Defaults: removeAdditional: false, useDefaults: false, coerceTypes: false, + // strict mode options + strictDefaults: false, + strictKeywords: false, + strictNumbers: false, // asynchronous validation options: - async: 'co*', transpile: undefined, // requires ajv-async package // advanced options: meta: true, @@ -1072,10 +1183,10 @@ Defaults: loopRequired: Infinity, ownProperties: false, multipleOfPrecision: false, - errorDataPath: 'object', + errorDataPath: 'object', // deprecated messages: true, sourceCode: false, - processCode: undefined, // function (str: string): string {} + processCode: undefined, // function (str: string, schema: object): string {} cache: new Cache, serialize: undefined } @@ -1086,24 +1197,36 @@ Defaults: - _$data_: support [$data references](#data-reference). Draft 6 meta-schema that is added by default will be extended to allow them. If you want to use another meta-schema you need to use $dataMetaSchema method to add support for $data reference. See [API](#api). - _allErrors_: check all rules collecting all errors. Default is to return after the first error. - _verbose_: include the reference to the part of the schema (`schema` and `parentSchema`) and validated data in errors (false by default). +- _$comment_ (NEW in Ajv version 6.0): log or pass the value of `$comment` keyword to a function. Option values: + - `false` (default): ignore $comment keyword. + - `true`: log the keyword value to console. + - function: pass the keyword value, its schema path and root schema to the specified function - _jsonPointers_: set `dataPath` property of errors using [JSON Pointers](https://tools.ietf.org/html/rfc6901) instead of JavaScript property access notation. - _uniqueItems_: validate `uniqueItems` keyword (true by default). - _unicode_: calculate correct length of strings with unicode pairs (true by default). Pass `false` to use `.length` of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters. -- _format_: formats validation mode ('fast' by default). Pass 'full' for more correct and slow validation or `false` not to validate formats at all. E.g., 25:00:00 and 2015/14/33 will be invalid time and date in 'full' mode but it will be valid in 'fast' mode. +- _nullable_: support keyword "nullable" from [Open API 3 specification](https://swagger.io/docs/specification/data-models/data-types/). +- _format_: formats validation mode. Option values: + - `"fast"` (default) - simplified and fast validation (see [Formats](#formats) for details of which formats are available and affected by this option). + - `"full"` - more restrictive and slow validation. E.g., 25:00:00 and 2015/14/33 will be invalid time and date in 'full' mode but it will be valid in 'fast' mode. + - `false` - ignore all format keywords. - _formats_: an object with custom formats. Keys and values will be passed to `addFormat` method. +- _keywords_: an object with custom keywords. Keys and values will be passed to `addKeyword` method. - _unknownFormats_: handling of unknown formats. Option values: - `true` (default) - if an unknown format is encountered the exception is thrown during schema compilation. If `format` keyword value is [$data reference](#data-reference) and it is unknown the validation will fail. - `[String]` - an array of unknown format names that will be ignored. This option can be used to allow usage of third party schemas with format(s) for which you don't have definitions, but still fail if another unknown format is used. If `format` keyword value is [$data reference](#data-reference) and it is not in this array the validation will fail. - - `"ignore"` - to log warning during schema compilation and always pass validation (the default behaviour in versions before 5.0.0). This option is not recommended, as it allows to mistype format name and it won't be validated without any error message. This behaviour is required by JSON-schema specification. + - `"ignore"` - to log warning during schema compilation and always pass validation (the default behaviour in versions before 5.0.0). This option is not recommended, as it allows to mistype format name and it won't be validated without any error message. This behaviour is required by JSON Schema specification. - _schemas_: an array or object of schemas that will be added to the instance. In case you pass the array the schemas must have IDs in them. When the object is passed the method `addSchema(value, key)` will be called for each schema in this object. +- _logger_: sets the logging method. Default is the global `console` object that should have methods `log`, `warn` and `error`. See [Error logging](#error-logging). Option values: + - custom logger - it should have methods `log`, `warn` and `error`. If any of these methods is missing an exception will be thrown. + - `false` - logging is disabled. ##### Referenced schema options - _schemaId_: this option defines which keywords are used as schema URI. Option value: - - `"$id"` (recommended) - only use `$id` keyword as schema URI (as specified in JSON Schema draft-06), ignore `id` keyword (if it is present a warning will be logged). + - `"$id"` (default) - only use `$id` keyword as schema URI (as specified in JSON Schema draft-06/07), ignore `id` keyword (if it is present a warning will be logged). - `"id"` - only use `id` keyword as schema URI (as specified in JSON Schema draft-04), ignore `$id` keyword (if it is present a warning will be logged). - - `undefined` (default) - use both `$id` and `id` keywords as schema URI. If both are present (in the same schema object) and different the exception will be thrown during schema compilation. + - `"auto"` - use both `$id` and `id` keywords as schema URI. If both are present (in the same schema object) and different the exception will be thrown during schema compilation. - _missingRefs_: handling of missing referenced schemas. Option values: - `true` (default) - if the reference cannot be resolved during compilation the exception is thrown. The thrown error has properties `missingRef` (with hash fragment) and `missingSchema` (without it). Both properties are resolved relative to the current base id (usually schema id, unless it was substituted). - `"ignore"` - to log error during compilation and always pass validation. @@ -1122,42 +1245,43 @@ Defaults: - `"all"` - all additional properties are removed, regardless of `additionalProperties` keyword in schema (and no validation is made for them). - `true` - only additional properties with `additionalProperties` keyword equal to `false` are removed. - `"failing"` - additional properties that fail schema validation will be removed (where `additionalProperties` keyword is `false` or schema). -- _useDefaults_: replace missing properties and items with the values from corresponding `default` keywords. Default behaviour is to ignore `default` keywords. This option is not used if schema is added with `addMetaSchema` method. See examples in [Assigning defaults](#assigning-defaults). Option values: +- _useDefaults_: replace missing or undefined properties and items with the values from corresponding `default` keywords. Default behaviour is to ignore `default` keywords. This option is not used if schema is added with `addMetaSchema` method. See examples in [Assigning defaults](#assigning-defaults). Option values: - `false` (default) - do not use defaults - - `true` - insert defaults by value (safer and slower, object literal is used). - - `"shared"` - insert defaults by reference (faster). If the default is an object, it will be shared by all instances of validated data. If you modify the inserted default in the validated data, it will be modified in the schema as well. -- _coerceTypes_: change data type of data to match `type` keyword. See the example in [Coercing data types](#coercing-data-types) and [coercion rules](https://github.com/epoberezkin/ajv/blob/master/COERCION.md). Option values: + - `true` - insert defaults by value (object literal is used). + - `"empty"` - in addition to missing or undefined, use defaults for properties and items that are equal to `null` or `""` (an empty string). + - `"shared"` (deprecated) - insert defaults by reference. If the default is an object, it will be shared by all instances of validated data. If you modify the inserted default in the validated data, it will be modified in the schema as well. +- _coerceTypes_: change data type of data to match `type` keyword. See the example in [Coercing data types](#coercing-data-types) and [coercion rules](https://github.com/ajv-validator/ajv/blob/master/COERCION.md). Option values: - `false` (default) - no type coercion. - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). +##### Strict mode options + +- _strictDefaults_: report ignored `default` keywords in schemas. Option values: + - `false` (default) - ignored defaults are not reported + - `true` - if an ignored default is present, throw an error + - `"log"` - if an ignored default is present, log warning +- _strictKeywords_: report unknown keywords in schemas. Option values: + - `false` (default) - unknown keywords are not reported + - `true` - if an unknown keyword is present, throw an error + - `"log"` - if an unknown keyword is present, log warning +- _strictNumbers_: validate numbers strictly, failing validation for NaN and Infinity. Option values: + - `false` (default) - NaN or Infinity will pass validation for numeric types + - `true` - NaN or Infinity will not pass validation for numeric types + ##### Asynchronous validation options -- _async_: determines how Ajv compiles asynchronous schemas (see [Asynchronous validation](#asynchronous-validation)) to functions. Option values: - - `"*"` / `"co*"` (default) - compile to generator function ("co*" - wrapped with `co.wrap`). If generators are not supported and you don't provide `processCode` option (or `transpile` option if you use [ajv-async](https://github.com/epoberezkin/ajv-async) package), the exception will be thrown when async schema is compiled. - - `"es7"` - compile to es7 async function. Unless your platform supports them you need to provide `processCode` or `transpile` option. According to [compatibility table](http://kangax.github.io/compat-table/es7/)) async functions are supported by: - - Firefox 52, - - Chrome 55, - - Node.js 7 (with `--harmony-async-await`), - - MS Edge 13 (with flag). - - `undefined`/`true` - auto-detect async mode. It requires [ajv-async](https://github.com/epoberezkin/ajv-async) package. If `transpile` option is not passed, ajv-async will choose the first of supported/installed async/transpile modes in this order: - - "es7" (native async functions), - - "co*" (native generators with co.wrap), - - "es7"/"nodent", - - "co*"/"regenerator" during the creation of the Ajv instance. - - If none of the options is available the exception will be thrown. -- _transpile_: Requires [ajv-async](https://github.com/epoberezkin/ajv-async) package. It determines whether Ajv transpiles compiled asynchronous validation function. Option values: - - `"nodent"` - transpile with [nodent](https://github.com/MatAtBread/nodent). If nodent is not installed, the exception will be thrown. nodent can only transpile es7 async functions; it will enforce this mode. - - `"regenerator"` - transpile with [regenerator](https://github.com/facebook/regenerator). If regenerator is not installed, the exception will be thrown. - - a function - this function should accept the code of validation function as a string and return transpiled code. This option allows you to use any other transpiler you prefer. If you are passing a function, you can simply pass it to `processCode` option without using ajv-async. +- _transpile_: Requires [ajv-async](https://github.com/ajv-validator/ajv-async) package. It determines whether Ajv transpiles compiled asynchronous validation function. Option values: + - `undefined` (default) - transpile with [nodent](https://github.com/MatAtBread/nodent) if async functions are not supported. + - `true` - always transpile with nodent. + - `false` - do not transpile; if async functions are not supported an exception will be thrown. ##### Advanced options - _meta_: add [meta-schema](http://json-schema.org/documentation.html) so it can be used by other schemas (true by default). If an object is passed, it will be used as the default meta-schema for schemas that have no `$schema` keyword. This default meta-schema MUST have `$schema` keyword. -- _validateSchema_: validate added/compiled schemas against meta-schema (true by default). `$schema` property in the schema can either be http://json-schema.org/schema or http://json-schema.org/draft-04/schema or absent (draft-4 meta-schema will be used) or can be a reference to the schema previously added with `addMetaSchema` method. Option values: +- _validateSchema_: validate added/compiled schemas against meta-schema (true by default). `$schema` property in the schema can be http://json-schema.org/draft-07/schema or absent (draft-07 meta-schema will be used) or can be a reference to the schema previously added with `addMetaSchema` method. Option values: - `true` (default) - if the validation fails, throw the exception. - `"log"` - if the validation fails, log error. - `false` - skip schema validation. @@ -1169,15 +1293,15 @@ Defaults: - _passContext_: pass validation context to custom keyword functions. If this option is `true` and you pass some context to the compiled validation function with `validate.call(context, data)`, the `context` will be available as `this` in your custom keywords. By default `this` is Ajv instance. - _loopRequired_: by default `required` keyword is compiled into a single expression (or a sequence of statements in `allErrors` mode). In case of a very large number of properties in this keyword it may result in a very big validation function. Pass integer to set the number of properties above which `required` keyword will be validated in a loop - smaller validation function size but also worse performance. - _ownProperties_: by default Ajv iterates over all enumerable object properties; when this option is `true` only own enumerable object properties (i.e. found directly on the object rather than on its prototype) are iterated. Contributed by @mbroadst. -- _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/epoberezkin/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). -- _errorDataPath_: set `dataPath` to point to 'object' (default) or to 'property' when validating keywords `required`, `additionalProperties` and `dependencies`. -- _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when custom messages are used (e.g. with [ajv-i18n](https://github.com/epoberezkin/ajv-i18n)). +- _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/ajv-validator/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). +- _errorDataPath_ (deprecated): set `dataPath` to point to 'object' (default) or to 'property' when validating keywords `required`, `additionalProperties` and `dependencies`. +- _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when custom messages are used (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). - _sourceCode_: add `sourceCode` property to validating function (for debugging; this code can be different from the result of toString call). - _processCode_: an optional function to process generated code before it is passed to Function constructor. It can be used to either beautify (the validating function is generated without line-breaks) or to transpile code. Starting from version 5.0.0 this option replaced options: - - `beautify` that formatted the generated function using [js-beautify](https://github.com/beautify-web/js-beautify). If you want to beautify the generated code pass `require('js-beautify').js_beautify`. - - `transpile` that transpiled asynchronous validation function. You can still use `transpile` option with [ajv-async](https://github.com/epoberezkin/ajv-async) package. See [Asynchronous validation](#asynchronous-validation) for more information. + - `beautify` that formatted the generated function using [js-beautify](https://github.com/beautify-web/js-beautify). If you want to beautify the generated code pass a function calling `require('js-beautify').js_beautify` as `processCode: code => js_beautify(code)`. + - `transpile` that transpiled asynchronous validation function. You can still use `transpile` option with [ajv-async](https://github.com/ajv-validator/ajv-async) package. See [Asynchronous validation](#asynchronous-validation) for more information. - _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)`, `del(key)` and `clear()`. -- _serialize_: an optional function to serialize schema to cache key. Pass `false` to use schema itself as a key (e.g., if WeakMap used as a cache). By default [json-stable-stringify](https://github.com/substack/json-stable-stringify) is used. +- _serialize_: an optional function to serialize schema to cache key. Pass `false` to use schema itself as a key (e.g., if WeakMap used as a cache). By default [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used. ## Validation errors @@ -1192,7 +1316,7 @@ Each error is an object with the following properties: - _keyword_: validation keyword. - _dataPath_: the path to the part of the data that was validated. By default `dataPath` uses JavaScript property access notation (e.g., `".prop[1].subProp"`). When the option `jsonPointers` is true (see [Options](#options)) `dataPath` will be set using JSON pointer standard (e.g., `"/prop/1/subProp"`). - _schemaPath_: the path (JSON-pointer as a URI fragment) to the schema of the keyword that failed validation. -- _params_: the object with the additional information about error that can be used to create custom error messages (e.g., using [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) package). See below for parameters set by all keywords. +- _params_: the object with the additional information about error that can be used to create custom error messages (e.g., using [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) package). See below for parameters set by all keywords. - _message_: the standard error message (can be excluded with option `messages` set to false). - _schema_: the schema of the keyword (added with `verbose` option). - _parentSchema_: the schema containing the keyword (added with `verbose` option) @@ -1225,20 +1349,59 @@ Properties of `params` object in errors depend on the keyword that failed valida - `patternRequired` (in ajv-keywords) - property `missingPattern` (required pattern that did not match any property). - `type` - property `type` (required type(s), a string, can be a comma-separated list) - `uniqueItems` - properties `i` and `j` (indices of duplicate items). +- `const` - property `allowedValue` pointing to the value (the schema of the keyword). - `enum` - property `allowedValues` pointing to the array of values (the schema of the keyword). - `$ref` - property `ref` with the referenced schema URI. +- `oneOf` - property `passingSchemas` (array of indices of passing schemas, null if no schema passes). - custom keywords (in case keyword definition doesn't create errors) - property `keyword` (the keyword name). -## Related packages +### Error logging + +Using the `logger` option when initiallizing Ajv will allow you to define custom logging. Here you can build upon the exisiting logging. The use of other logging packages is supported as long as the package or its associated wrapper exposes the required methods. If any of the required methods are missing an exception will be thrown. +- **Required Methods**: `log`, `warn`, `error` -- [ajv-cli](https://github.com/epoberezkin/ajv-cli) - command line interface for Ajv -- [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) - internationalised error messages -- [ajv-merge-patch](https://github.com/epoberezkin/ajv-merge-patch) - keywords $merge and $patch. -- [ajv-keywords](https://github.com/epoberezkin/ajv-keywords) - several custom keywords that can be used with Ajv (typeof, instanceof, range, propertyNames) -- [ajv-errors](https://github.com/epoberezkin/ajv-errors) - custom error messages for Ajv +```javascript +var otherLogger = new OtherLogger(); +var ajv = new Ajv({ + logger: { + log: console.log.bind(console), + warn: function warn() { + otherLogger.logWarn.apply(otherLogger, arguments); + }, + error: function error() { + otherLogger.logError.apply(otherLogger, arguments); + console.error.apply(console, arguments); + } + } +}); +``` +## Plugins + +Ajv can be extended with plugins that add custom keywords, formats or functions to process generated code. When such plugin is published as npm package it is recommended that it follows these conventions: + +- it exports a function +- this function accepts ajv instance as the first parameter and returns the same instance to allow chaining +- this function can accept an optional configuration as the second parameter + +If you have published a useful plugin please submit a PR to add it to the next section. + + +## Related packages + +- [ajv-async](https://github.com/ajv-validator/ajv-async) - plugin to configure async validation mode +- [ajv-bsontype](https://github.com/BoLaMN/ajv-bsontype) - plugin to validate mongodb's bsonType formats +- [ajv-cli](https://github.com/jessedc/ajv-cli) - command line interface +- [ajv-errors](https://github.com/ajv-validator/ajv-errors) - plugin for custom error messages +- [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) - internationalised error messages +- [ajv-istanbul](https://github.com/ajv-validator/ajv-istanbul) - plugin to instrument generated validation code to measure test coverage of your schemas +- [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) - plugin with custom validation keywords (select, typeof, etc.) +- [ajv-merge-patch](https://github.com/ajv-validator/ajv-merge-patch) - plugin with keywords $merge and $patch +- [ajv-pack](https://github.com/ajv-validator/ajv-pack) - produces a compact module exporting validation functions +- [ajv-formats-draft2019](https://github.com/luzlab/ajv-formats-draft2019) - format validators for draft2019 that aren't already included in ajv (ie. `idn-hostname`, `idn-email`, `iri`, `iri-reference` and `duration`). + ## Some packages using Ajv - [webpack](https://github.com/webpack/webpack) - a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser @@ -1246,7 +1409,7 @@ Properties of `params` object in errors depend on the keyword that failed valida - [osprey-method-handler](https://github.com/mulesoft-labs/osprey-method-handler) - Express middleware for validating requests and responses based on a RAML method object, used in [osprey](https://github.com/mulesoft/osprey) - validating API proxy generated from a RAML definition - [har-validator](https://github.com/ahmadnassri/har-validator) - HTTP Archive (HAR) validator - [jsoneditor](https://github.com/josdejong/jsoneditor) - a web-based tool to view, edit, format, and validate JSON http://jsoneditoronline.org -- [JSON Schema Lint](https://github.com/nickcmaynard/jsonschemalint) - a web tool to validate JSON/YAML document against a single JSON-schema http://jsonschemalint.com +- [JSON Schema Lint](https://github.com/nickcmaynard/jsonschemalint) - a web tool to validate JSON/YAML document against a single JSON Schema http://jsonschemalint.com - [objection](https://github.com/vincit/objection.js) - SQL-friendly ORM for Node.js - [table](https://github.com/gajus/table) - formats data into a string table - [ripple-lib](https://github.com/ripple/ripple-lib) - a JavaScript API for interacting with [Ripple](https://ripple.com) in Node.js and the browser @@ -1255,12 +1418,13 @@ Properties of `params` object in errors depend on the keyword that failed valida - [react-form-controlled](https://github.com/seeden/react-form-controlled) - React controlled form components with validation - [rabbitmq-schema](https://github.com/tjmehta/rabbitmq-schema) - a schema definition module for RabbitMQ graphs and messages - [@query/schema](https://www.npmjs.com/package/@query/schema) - stream filtering with a URI-safe query syntax parsing to JSON Schema -- [chai-ajv-json-schema](https://github.com/peon374/chai-ajv-json-schema) - chai plugin to us JSON-schema with expect in mocha tests +- [chai-ajv-json-schema](https://github.com/peon374/chai-ajv-json-schema) - chai plugin to us JSON Schema with expect in mocha tests - [grunt-jsonschema-ajv](https://github.com/SignpostMarv/grunt-jsonschema-ajv) - Grunt plugin for validating files against JSON Schema - [extract-text-webpack-plugin](https://github.com/webpack-contrib/extract-text-webpack-plugin) - extract text from bundle into a file - [electron-builder](https://github.com/electron-userland/electron-builder) - a solution to package and build a ready for distribution Electron app - [addons-linter](https://github.com/mozilla/addons-linter) - Mozilla Add-ons Linter - [gh-pages-generator](https://github.com/epoberezkin/gh-pages-generator) - multi-page site generator converting markdown files to GitHub pages +- [ESLint](https://github.com/eslint/eslint) - the pluggable linting utility for JavaScript and JSX ## Tests @@ -1273,30 +1437,42 @@ npm test ## Contributing -All validation functions are generated using doT templates in [dot](https://github.com/epoberezkin/ajv/tree/master/lib/dot) folder. Templates are precompiled so doT is not a run-time dependency. +All validation functions are generated using doT templates in [dot](https://github.com/ajv-validator/ajv/tree/master/lib/dot) folder. Templates are precompiled so doT is not a run-time dependency. -`npm run build` - compiles templates to [dotjs](https://github.com/epoberezkin/ajv/tree/master/lib/dotjs) folder. +`npm run build` - compiles templates to [dotjs](https://github.com/ajv-validator/ajv/tree/master/lib/dotjs) folder. `npm run watch` - automatically compiles templates when files in dot folder change -Please see [Contributing guidelines](https://github.com/epoberezkin/ajv/blob/master/CONTRIBUTING.md) +Please see [Contributing guidelines](https://github.com/ajv-validator/ajv/blob/master/CONTRIBUTING.md) ## Changes history -See https://github.com/epoberezkin/ajv/releases +See https://github.com/ajv-validator/ajv/releases + +__Please note__: [Changes in version 6.0.0](https://github.com/ajv-validator/ajv/releases/tag/v6.0.0). + +[Version 5.0.0](https://github.com/ajv-validator/ajv/releases/tag/5.0.0). + +[Version 4.0.0](https://github.com/ajv-validator/ajv/releases/tag/4.0.0). + +[Version 3.0.0](https://github.com/ajv-validator/ajv/releases/tag/3.0.0). + +[Version 2.0.0](https://github.com/ajv-validator/ajv/releases/tag/2.0.0). + + +## Code of conduct -__Please note__: [Changes in version 5.0.0](https://github.com/epoberezkin/ajv/releases/tag/5.0.0). +Please review and follow the [Code of conduct](https://github.com/ajv-validator/ajv/blob/master/CODE_OF_CONDUCT.md). -[Changes in version 4.6.0](https://github.com/epoberezkin/ajv/releases/tag/4.6.0). +Please report any unacceptable behaviour to ajv.validator@gmail.com - it will be reviewed by the project team. -[Changes in version 4.0.0](https://github.com/epoberezkin/ajv/releases/tag/4.0.0). -[Changes in version 3.0.0](https://github.com/epoberezkin/ajv/releases/tag/3.0.0). +## Open-source software support -[Changes in version 2.0.0](https://github.com/epoberezkin/ajv/releases/tag/2.0.0). +Ajv is a part of [Tidelift subscription](https://tidelift.com/subscription/pkg/npm-ajv?utm_source=npm-ajv&utm_medium=referral&utm_campaign=readme) - it provides a centralised support to open-source software users, in addition to the support provided by software maintainers. ## License -[MIT](https://github.com/epoberezkin/ajv/blob/master/LICENSE) +[MIT](https://github.com/ajv-validator/ajv/blob/master/LICENSE) diff --git a/bower.json b/bower.json index 048c089b6..507989c62 100644 --- a/bower.json +++ b/bower.json @@ -11,7 +11,7 @@ "schema", "validator" ], - "homepage": "https://github.com/epoberezkin/ajv", + "homepage": "https://github.com/ajv-validator/ajv", "moduleType": [ "amd", "globals", diff --git a/karma.conf.js b/karma.conf.js index 1ffca9b0f..155d0874e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -17,8 +17,7 @@ module.exports = function(config) { files: [ 'dist/ajv.min.js', 'node_modules/chai/chai.js', - 'dist/regenerator.min.js', - 'dist/nodent.min.js', + 'node_modules/ajv-async/dist/ajv-async.min.js', 'node_modules/bluebird/js/browser/bluebird.core.min.js', '.browser/*.spec.js' ], @@ -54,8 +53,13 @@ module.exports = function(config) { // - Safari (only Mac) // - PhantomJS // - IE (only Windows) - browsers: ['Chrome'], - + browsers: ['HeadlessChrome'], + customLaunchers: { + HeadlessChrome:{ + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + }, + }, // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits diff --git a/karma.sauce.js b/karma.sauce.js index 1316ab0e5..fac172e36 100644 --- a/karma.sauce.js +++ b/karma.sauce.js @@ -22,29 +22,18 @@ module.exports = function(config) { browserName: 'chrome', version: '27' }, - // 'SL_Chrome_37': { - // base: 'SauceLabs', - // browserName: 'chrome', - // version: '37' - // }, 'SL_Chrome': { base: 'SauceLabs', browserName: 'chrome' }, - 'SL_InternetExplorer_9': { - base: 'SauceLabs', - browserName: 'internet explorer', - version: '9' - }, 'SL_InternetExplorer_10': { base: 'SauceLabs', browserName: 'internet explorer', version: '10' }, - 'SL_InternetExplorer_11': { + 'SL_InternetExplorer': { base: 'SauceLabs', - browserName: 'internet explorer', - version: '11' // default + browserName: 'internet explorer' }, 'SL_MicrosoftEdge': { base: 'SauceLabs', @@ -55,45 +44,32 @@ module.exports = function(config) { browserName: 'firefox', version: '17' }, - // 'SL_FireFox_24': { - // base: 'SauceLabs', - // browserName: 'firefox', - // version: '24' - // }, 'SL_FireFox': { base: 'SauceLabs', browserName: 'firefox' }, - 'SL_Safari_5': { + 'SL_Safari_7': { base: 'SauceLabs', browserName: 'safari', - version: '5' // default + version: '7' }, - // 'SL_Safari_7': { - // base: 'SauceLabs', - // browserName: 'safari', - // version: '7' - // }, - 'SL_Safari_9': { + 'SL_Safari': { base: 'SauceLabs', - browserName: 'safari', - version: '9' + browserName: 'safari' }, 'SL_iPhone_8': { base: 'SauceLabs', browserName: 'iphone', version: '8.4' }, - 'SL_iPhone_9': { + 'SL_iPhone': { base: 'SauceLabs', - browserName: 'iphone', - version: '9.2' + browserName: 'iphone' + }, + 'SL_Android': { + base: 'SauceLabs', + browserName: 'android' } - // 'SL_Android_4': { - // base: 'SauceLabs', - // browserName: 'android', - // version: '4' - // } }; @@ -112,7 +88,7 @@ module.exports = function(config) { files: [ 'dist/ajv.min.js', 'node_modules/chai/chai.js', - 'dist/nodent.min.js', + 'node_modules/ajv-async/dist/ajv-async.min.js', 'node_modules/bluebird/js/browser/bluebird.core.min.js', '.browser/*.spec.js' ], diff --git a/lib/ajv.d.ts b/lib/ajv.d.ts index 2a8ee1898..cc2881b33 100644 --- a/lib/ajv.d.ts +++ b/lib/ajv.d.ts @@ -1,125 +1,165 @@ -declare var ajv: { +declare var ajv: { (options?: ajv.Options): ajv.Ajv; - new (options?: ajv.Options): ajv.Ajv; - ValidationError: ValidationError; - MissingRefError: MissingRefError; - $dataMetaSchema: Object; + new(options?: ajv.Options): ajv.Ajv; + ValidationError: typeof AjvErrors.ValidationError; + MissingRefError: typeof AjvErrors.MissingRefError; + $dataMetaSchema: object; +} + +declare namespace AjvErrors { + class ValidationError extends Error { + constructor(errors: Array); + + message: string; + errors: Array; + ajv: true; + validation: true; + } + + class MissingRefError extends Error { + constructor(baseId: string, ref: string, message?: string); + static message: (baseId: string, ref: string) => string; + + message: string; + missingRef: string; + missingSchema: string; + } } declare namespace ajv { + type ValidationError = AjvErrors.ValidationError; + + type MissingRefError = AjvErrors.MissingRefError; + interface Ajv { /** * Validate data using schema - * Schema will be compiled and cached (using serialized JSON as key, [json-stable-stringify](https://github.com/substack/json-stable-stringify) is used to serialize by default). - * @param {String|Object|Boolean} schemaKeyRef key, ref or schema object + * Schema will be compiled and cached (using serialized JSON as key, [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize by default). + * @param {string|object|Boolean} schemaKeyRef key, ref or schema object * @param {Any} data to be validated * @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`). */ - validate(schemaKeyRef: Object | string | boolean, data: any): boolean | Thenable; + validate(schemaKeyRef: object | string | boolean, data: any): boolean | PromiseLike; /** * Create validating function for passed schema. - * @param {Object|Boolean} schema schema object + * @param {object|Boolean} schema schema object * @return {Function} validating function */ - compile(schema: Object | boolean): ValidateFunction; + compile(schema: object | boolean): ValidateFunction; /** * Creates validating function for passed schema with asynchronous loading of missing schemas. * `loadSchema` option should be a function that accepts schema uri and node-style callback. * @this Ajv - * @param {Object|Boolean} schema schema object + * @param {object|Boolean} schema schema object * @param {Boolean} meta optional true to compile meta-schema; this parameter can be skipped * @param {Function} callback optional node-style callback, it is always called with 2 parameters: error (or null) and validating function. - * @return {Thenable} validating function + * @return {PromiseLike} validating function */ - compileAsync(schema: Object | boolean, meta?: Boolean, callback?: (err: Error, validate: ValidateFunction) => any): Thenable; + compileAsync(schema: object | boolean, meta?: Boolean, callback?: (err: Error, validate: ValidateFunction) => any): PromiseLike; /** * Adds schema to the instance. - * @param {Object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored. - * @param {String} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. + * @param {object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored. + * @param {string} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. + * @return {Ajv} this for method chaining */ - addSchema(schema: Array | Object, key?: string): void; + addSchema(schema: Array | object, key?: string): Ajv; /** * Add schema that will be used to validate other schemas * options in META_IGNORE_OPTIONS are alway set to false - * @param {Object} schema schema object - * @param {String} key optional schema key + * @param {object} schema schema object + * @param {string} key optional schema key + * @return {Ajv} this for method chaining */ - addMetaSchema(schema: Object, key?: string): void; + addMetaSchema(schema: object, key?: string): Ajv; /** * Validate schema - * @param {Object|Boolean} schema schema to validate + * @param {object|Boolean} schema schema to validate * @return {Boolean} true if schema is valid */ - validateSchema(schema: Object | boolean): boolean; + validateSchema(schema: object | boolean): boolean; /** * Get compiled schema from the instance by `key` or `ref`. - * @param {String} keyRef `key` that was passed to `addSchema` or full schema reference (`schema.id` or resolved id). - * @return {Function} schema validating function (with property `schema`). + * @param {string} keyRef `key` that was passed to `addSchema` or full schema reference (`schema.id` or resolved id). + * @return {Function} schema validating function (with property `schema`). Returns undefined if keyRef can't be resolved to an existing schema. */ - getSchema(keyRef: string): ValidateFunction; + getSchema(keyRef: string): ValidateFunction | undefined; /** * Remove cached schema(s). * If no parameter is passed all schemas but meta-schemas are removed. * If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed. * Even if schema is referenced by other schemas it still can be removed as other schemas have local references. - * @param {String|Object|RegExp|Boolean} schemaKeyRef key, ref, pattern to match key/ref or schema object + * @param {string|object|RegExp|Boolean} schemaKeyRef key, ref, pattern to match key/ref or schema object + * @return {Ajv} this for method chaining */ - removeSchema(schemaKeyRef?: Object | string | RegExp | boolean): void; + removeSchema(schemaKeyRef?: object | string | RegExp | boolean): Ajv; /** * Add custom format - * @param {String} name format name - * @param {String|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid) + * @param {string} name format name + * @param {string|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid) + * @return {Ajv} this for method chaining */ - addFormat(name: string, format: FormatValidator | FormatDefinition): void; + addFormat(name: string, format: FormatValidator | FormatDefinition): Ajv; /** * Define custom keyword * @this Ajv - * @param {String} keyword custom keyword, should be a valid identifier, should be different from all standard, custom and macro keywords. - * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. + * @param {string} keyword custom keyword, should be a valid identifier, should be different from all standard, custom and macro keywords. + * @param {object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. + * @return {Ajv} this for method chaining */ - addKeyword(keyword: string, definition: KeywordDefinition): void; + addKeyword(keyword: string, definition: KeywordDefinition): Ajv; /** * Get keyword definition * @this Ajv - * @param {String} keyword pre-defined or custom keyword. - * @return {Object|Boolean} custom keyword definition, `true` if it is a predefined keyword, `false` otherwise. + * @param {string} keyword pre-defined or custom keyword. + * @return {object|Boolean} custom keyword definition, `true` if it is a predefined keyword, `false` otherwise. */ - getKeyword(keyword: string): Object | boolean; + getKeyword(keyword: string): object | boolean; /** * Remove keyword * @this Ajv - * @param {String} keyword pre-defined or custom keyword. + * @param {string} keyword pre-defined or custom keyword. + * @return {Ajv} this for method chaining + */ + removeKeyword(keyword: string): Ajv; + /** + * Validate keyword + * @this Ajv + * @param {object} definition keyword definition object + * @param {boolean} throwError true to throw exception if definition is invalid + * @return {boolean} validation result */ - removeKeyword(keyword: string): void; + validateKeyword(definition: KeywordDefinition, throwError: boolean): boolean; /** * Convert array of error message objects to string - * @param {Array} errors optional array of validation errors, if not passed errors from the instance are used. - * @param {Object} options optional options with properties `separator` and `dataVar`. - * @return {String} human readable string with all errors descriptions + * @param {Array} errors optional array of validation errors, if not passed errors from the instance are used. + * @param {object} options optional options with properties `separator` and `dataVar`. + * @return {string} human readable string with all errors descriptions */ - errorsText(errors?: Array, options?: ErrorsTextOptions): string; - errors?: Array; + errorsText(errors?: Array | null, options?: ErrorsTextOptions): string; + errors?: Array | null; } - interface Thenable { - then (onFulfilled?: (value: R) => U | Thenable, onRejected?: (error: any) => U | Thenable): Thenable; + interface CustomLogger { + log(...args: any[]): any; + warn(...args: any[]): any; + error(...args: any[]): any; } interface ValidateFunction { ( data: any, dataPath?: string, - parentData?: Object | Array, + parentData?: object | Array, parentDataProperty?: string | number, - rootData?: Object | Array - ): boolean | Thenable; - schema?: Object | boolean; + rootData?: object | Array + ): boolean | PromiseLike; + schema?: object | boolean; errors?: null | Array; - refs?: Object; + refs?: object; refVal?: Array; - root?: ValidateFunction | Object; + root?: ValidateFunction | object; $async?: true; - source?: Object; + source?: object; } interface Options { @@ -129,20 +169,24 @@ declare namespace ajv { jsonPointers?: boolean; uniqueItems?: boolean; unicode?: boolean; - format?: string; - formats?: Object; + format?: false | string; + formats?: object; + keywords?: object; unknownFormats?: true | string[] | 'ignore'; - schemas?: Array | Object; - schemaId?: '$id' | 'id'; + schemas?: Array | object; + schemaId?: '$id' | 'id' | 'auto'; missingRefs?: true | 'ignore' | 'fail'; extendRefs?: true | 'ignore' | 'fail'; - loadSchema?: (uri: string, cb?: (err: Error, schema: Object) => void) => Thenable; + loadSchema?: (uri: string, cb?: (err: Error, schema: object) => void) => PromiseLike; removeAdditional?: boolean | 'all' | 'failing'; - useDefaults?: boolean | 'shared'; + useDefaults?: boolean | 'empty' | 'shared'; coerceTypes?: boolean | 'array'; + strictDefaults?: boolean | 'log'; + strictKeywords?: boolean | 'log'; + strictNumbers?: boolean; async?: boolean | string; transpile?: string | ((code: string) => string); - meta?: boolean | Object; + meta?: boolean | object; validateSchema?: boolean | 'log'; addUsedSchema?: boolean; inlineRefs?: boolean | number; @@ -153,45 +197,95 @@ declare namespace ajv { errorDataPath?: string, messages?: boolean; sourceCode?: boolean; - processCode?: (code: string) => string; - cache?: Object; + processCode?: (code: string, schema: object) => string; + cache?: object; + logger?: CustomLogger | false; + nullable?: boolean; + serialize?: ((schema: object | boolean) => any) | false; } - type FormatValidator = string | RegExp | ((data: string) => boolean | Thenable); + type FormatValidator = string | RegExp | ((data: string) => boolean | PromiseLike); + type NumberFormatValidator = ((data: number) => boolean | PromiseLike); + + interface NumberFormatDefinition { + type: "number", + validate: NumberFormatValidator; + compare?: (data1: number, data2: number) => number; + async?: boolean; + } - interface FormatDefinition { + interface StringFormatDefinition { + type?: "string", validate: FormatValidator; - compare: (data1: string, data2: string) => number; + compare?: (data1: string, data2: string) => number; async?: boolean; } + type FormatDefinition = NumberFormatDefinition | StringFormatDefinition; + interface KeywordDefinition { type?: string | Array; async?: boolean; $data?: boolean; errors?: boolean | string; - metaSchema?: Object; + metaSchema?: object; // schema: false makes validate not to expect schema (ValidateFunction) schema?: boolean; + statements?: boolean; + dependencies?: Array; modifying?: boolean; valid?: boolean; // one and only one of the following properties should be present validate?: SchemaValidateFunction | ValidateFunction; - compile?: (schema: any, parentSchema: Object) => ValidateFunction; - macro?: (schema: any, parentSchema: Object) => Object | boolean; - inline?: (it: Object, keyword: string, schema: any, parentSchema: Object) => string; + compile?: (schema: any, parentSchema: object, it: CompilationContext) => ValidateFunction; + macro?: (schema: any, parentSchema: object, it: CompilationContext) => object | boolean; + inline?: (it: CompilationContext, keyword: string, schema: any, parentSchema: object) => string; + } + + interface CompilationContext { + level: number; + dataLevel: number; + dataPathArr: string[]; + schema: any; + schemaPath: string; + baseId: string; + async: boolean; + opts: Options; + formats: { + [index: string]: FormatDefinition | undefined; + }; + keywords: { + [index: string]: KeywordDefinition | undefined; + }; + compositeRule: boolean; + validate: (schema: object) => boolean; + util: { + copy(obj: any, target?: any): any; + toHash(source: string[]): { [index: string]: true | undefined }; + equal(obj: any, target: any): boolean; + getProperty(str: string): string; + schemaHasRules(schema: object, rules: any): string; + escapeQuotes(str: string): string; + toQuotedString(str: string): string; + getData(jsonPointer: string, dataLevel: number, paths: string[]): string; + escapeJsonPointer(str: string): string; + unescapeJsonPointer(str: string): string; + escapeFragment(str: string): string; + unescapeFragment(str: string): string; + }; + self: Ajv; } interface SchemaValidateFunction { ( schema: any, data: any, - parentSchema?: Object, + parentSchema?: object, dataPath?: string, - parentData?: Object | Array, + parentData?: object | Array, parentDataProperty?: string | number, - rootData?: Object | Array - ): boolean | Thenable; + rootData?: object | Array + ): boolean | PromiseLike; errors?: Array; } @@ -211,16 +305,16 @@ declare namespace ajv { message?: string; // These are added with the `verbose` option. schema?: any; - parentSchema?: Object; + parentSchema?: object; data?: any; } type ErrorParameters = RefParams | LimitParams | AdditionalPropertiesParams | - DependenciesParams | FormatParams | ComparisonParams | - MultipleOfParams | PatternParams | RequiredParams | - TypeParams | UniqueItemsParams | CustomParams | - PatternGroupsParams | PatternRequiredParams | - PropertyNamesParams | SwitchParams | NoParams | EnumParams; + DependenciesParams | FormatParams | ComparisonParams | + MultipleOfParams | PatternParams | RequiredParams | + TypeParams | UniqueItemsParams | CustomParams | + PatternRequiredParams | PropertyNamesParams | + IfParams | SwitchParams | NoParams | EnumParams; interface RefParams { ref: string; @@ -276,12 +370,6 @@ declare namespace ajv { keyword: string; } - interface PatternGroupsParams { - reason: string; - limit: number; - pattern: string; - } - interface PatternRequiredParams { missingPattern: string; } @@ -290,33 +378,19 @@ declare namespace ajv { propertyName: string; } + interface IfParams { + failingKeyword: string; + } + interface SwitchParams { caseIndex: number; } - interface NoParams {} + interface NoParams { } interface EnumParams { allowedValues: Array; } } -declare class ValidationError extends Error { - constructor(errors: Array); - - message: string; - errors: Array; - ajv: true; - validation: true; -} - -declare class MissingRefError extends Error { - constructor(baseId: string, ref: string, message?: string); - static message: (baseId: string, ref: string) => string; - - message: string; - missingRef: string; - missingSchema: string; -} - export = ajv; diff --git a/lib/ajv.js b/lib/ajv.js index 14095599e..06a45b650 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -4,13 +4,11 @@ var compileSchema = require('./compile') , resolve = require('./compile/resolve') , Cache = require('./cache') , SchemaObject = require('./compile/schema_obj') - , stableStringify = require('json-stable-stringify') + , stableStringify = require('fast-json-stable-stringify') , formats = require('./compile/formats') , rules = require('./compile/rules') - , $dataMetaSchema = require('./$data') - , patternGroups = require('./patternGroups') - , util = require('./compile/util') - , co = require('co'); + , $dataMetaSchema = require('./data') + , util = require('./compile/util'); module.exports = Ajv; @@ -32,15 +30,16 @@ var customKeyword = require('./keyword'); Ajv.prototype.addKeyword = customKeyword.add; Ajv.prototype.getKeyword = customKeyword.get; Ajv.prototype.removeKeyword = customKeyword.remove; +Ajv.prototype.validateKeyword = customKeyword.validate; var errorClasses = require('./compile/error_classes'); Ajv.ValidationError = errorClasses.Validation; Ajv.MissingRefError = errorClasses.MissingRef; Ajv.$dataMetaSchema = $dataMetaSchema; -var META_SCHEMA_ID = 'http://json-schema.org/draft-06/schema'; +var META_SCHEMA_ID = 'http://json-schema.org/draft-07/schema'; -var META_IGNORE_OPTIONS = [ 'removeAdditional', 'useDefaults', 'coerceTypes' ]; +var META_IGNORE_OPTIONS = [ 'removeAdditional', 'useDefaults', 'coerceTypes', 'strictDefaults' ]; var META_SUPPORT_DATA = ['/properties']; /** @@ -52,12 +51,11 @@ var META_SUPPORT_DATA = ['/properties']; function Ajv(opts) { if (!(this instanceof Ajv)) return new Ajv(opts); opts = this._opts = util.copy(opts) || {}; + setLogger(this); this._schemas = {}; this._refs = {}; this._fragments = {}; this._formats = formats(opts.format); - var schemaUriFormat = this._schemaUriFormat = this._formats['uri-reference']; - this._schemaUriFormatFunc = function (str) { return schemaUriFormat.test(str); }; this._cache = opts.cache || new Cache; this._loadingSchemas = {}; @@ -71,17 +69,18 @@ function Ajv(opts) { this._metaOpts = getMetaSchemaOptions(this); if (opts.formats) addInitialFormats(this); - addDraft6MetaSchema(this); + if (opts.keywords) addInitialKeywords(this); + addDefaultMetaSchema(this); if (typeof opts.meta == 'object') this.addMetaSchema(opts.meta); + if (opts.nullable) this.addKeyword('nullable', {metaSchema: {type: 'boolean'}}); addInitialSchemas(this); - if (opts.patternGroups) patternGroups(this); } /** * Validate data using schema - * Schema will be compiled and cached (using serialized JSON as key. [json-stable-stringify](https://github.com/substack/json-stable-stringify) is used to serialize. + * Schema will be compiled and cached (using serialized JSON as key. [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize. * @this Ajv * @param {String|Object} schemaKeyRef key, ref or schema object * @param {Any} data to be validated @@ -98,9 +97,7 @@ function validate(schemaKeyRef, data) { } var valid = v(data); - if (v.$async === true) - return this._opts.async == '*' ? co(valid) : valid; - this.errors = v.errors; + if (v.$async !== true) this.errors = v.errors; return valid; } @@ -125,11 +122,12 @@ function compile(schema, _meta) { * @param {String} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. * @param {Boolean} _skipValidation true to skip schema validation. Used internally, option validateSchema should be used instead. * @param {Boolean} _meta true if schema is a meta-schema. Used internally, addMetaSchema should be used instead. + * @return {Ajv} this for method chaining */ function addSchema(schema, key, _skipValidation, _meta) { if (Array.isArray(schema)){ for (var i=0; i%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@| // var URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)(?:\.(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu; var URL = /^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i; var UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i; -var JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$|^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i; +var JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$/; +var JSON_POINTER_URI_FRAGMENT = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i; var RELATIVE_JSON_POINTER = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/; @@ -32,11 +33,11 @@ formats.fast = { // date: http://tools.ietf.org/html/rfc3339#section-5.6 date: /^\d\d\d\d-[0-1]\d-[0-3]\d$/, // date-time: http://tools.ietf.org/html/rfc3339#section-5.6 - time: /^[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:z|[+-]\d\d:\d\d)?$/i, - 'date-time': /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s][0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:z|[+-]\d\d:\d\d)$/i, + time: /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, + 'date-time': /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, // uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js - uri: /^(?:[a-z][a-z0-9+-.]*)(?::|\/)\/?[^\s]*$/i, - 'uri-reference': /^(?:(?:[a-z][a-z0-9+-.]*:)?\/\/)?[^\s]*$/i, + uri: /^(?:[a-z][a-z0-9+-.]*:)(?:\/?\/)?[^\s]*$/i, + 'uri-reference': /^(?:(?:[a-z][a-z0-9+-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, 'uri-template': URITEMPLATE, url: URL, // email (sources from jsen validator): @@ -54,6 +55,7 @@ formats.fast = { // JSON-pointer: https://tools.ietf.org/html/rfc6901 // uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A 'json-pointer': JSON_POINTER, + 'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT, // relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00 'relative-json-pointer': RELATIVE_JSON_POINTER }; @@ -67,25 +69,35 @@ formats.full = { 'uri-reference': URIREF, 'uri-template': URITEMPLATE, url: URL, - email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&''*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, - hostname: hostname, + email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, + hostname: HOSTNAME, ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, regex: regex, uuid: UUID, 'json-pointer': JSON_POINTER, + 'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT, 'relative-json-pointer': RELATIVE_JSON_POINTER }; +function isLeapYear(year) { + // https://tools.ietf.org/html/rfc3339#appendix-C + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} + + function date(str) { // full-date from http://tools.ietf.org/html/rfc3339#section-5.6 var matches = str.match(DATE); if (!matches) return false; - var month = +matches[1]; - var day = +matches[2]; - return month >= 1 && month <= 12 && day >= 1 && day <= DAYS[month]; + var year = +matches[1]; + var month = +matches[2]; + var day = +matches[3]; + + return month >= 1 && month <= 12 && day >= 1 && + day <= (month == 2 && isLeapYear(year) ? 29 : DAYS[month]); } @@ -97,7 +109,9 @@ function time(str, full) { var minute = matches[2]; var second = matches[3]; var timeZone = matches[5]; - return hour <= 23 && minute <= 59 && second <= 59 && (!full || timeZone); + return ((hour <= 23 && minute <= 59 && second <= 59) || + (hour == 23 && minute == 59 && second == 60)) && + (!full || timeZone); } @@ -109,13 +123,6 @@ function date_time(str) { } -function hostname(str) { - // https://tools.ietf.org/html/rfc1034#section-3.5 - // https://tools.ietf.org/html/rfc1123#section-2 - return str.length <= 255 && HOSTNAME.test(str); -} - - var NOT_URI_FRAGMENT = /\/|:/; function uri(str) { // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "." diff --git a/lib/compile/index.js b/lib/compile/index.js index 45e35d905..97518c424 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -3,7 +3,7 @@ var resolve = require('./resolve') , util = require('./util') , errorClasses = require('./error_classes') - , stableStringify = require('json-stable-stringify'); + , stableStringify = require('fast-json-stable-stringify'); var validateGenerator = require('../dotjs/validate'); @@ -11,7 +11,6 @@ var validateGenerator = require('../dotjs/validate'); * Functions below are used inside compiled validations function */ -var co = require('co'); var ucs2length = util.ucs2length; var equal = require('fast-deep-equal'); @@ -70,9 +69,11 @@ function compile(schema, root, localRefs, baseId) { endCompiling.call(this, schema, root, baseId); } + /* @this {*} - custom context, see passContext option */ function callValidate() { + /* jshint validthis: true */ var validate = compilation.validate; - var result = validate.apply(null, arguments); + var result = validate.apply(this, arguments); callValidate.errors = validate.errors; return result; } @@ -104,6 +105,7 @@ function compile(schema, root, localRefs, baseId) { useCustomRule: useCustomRule, opts: opts, formats: formats, + logger: self.logger, self: self }); @@ -111,7 +113,7 @@ function compile(schema, root, localRefs, baseId) { + vars(defaults, defaultCode) + vars(customRules, customRuleCode) + sourceCode; - if (opts.processCode) sourceCode = opts.processCode(sourceCode); + if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema); // console.log('\n\n\n *** \n', JSON.stringify(sourceCode)); var validate; try { @@ -123,7 +125,6 @@ function compile(schema, root, localRefs, baseId) { 'refVal', 'defaults', 'customRules', - 'co', 'equal', 'ucs2length', 'ValidationError', @@ -138,7 +139,6 @@ function compile(schema, root, localRefs, baseId) { refVal, defaults, customRules, - co, equal, ucs2length, ValidationError @@ -146,7 +146,7 @@ function compile(schema, root, localRefs, baseId) { refVal[0] = validate; } catch(e) { - console.error('Error compiling schema, function code:', sourceCode); + self.logger.error('Error compiling schema, function code:', sourceCode); throw e; } @@ -223,7 +223,7 @@ function compile(schema, root, localRefs, baseId) { function resolvedRef(refVal, code) { return typeof refVal == 'object' || typeof refVal == 'boolean' ? { code: code, schema: refVal, inline: true } - : { code: code, $async: refVal && refVal.$async }; + : { code: code, $async: refVal && !!refVal.$async }; } function usePattern(regexStr) { @@ -255,13 +255,21 @@ function compile(schema, root, localRefs, baseId) { } function useCustomRule(rule, schema, parentSchema, it) { - var validateSchema = rule.definition.validateSchema; - if (validateSchema && self._opts.validateSchema !== false) { - var valid = validateSchema(schema); - if (!valid) { - var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors); - if (self._opts.validateSchema == 'log') console.error(message); - else throw new Error(message); + if (self._opts.validateSchema !== false) { + var deps = rule.definition.dependencies; + if (deps && !deps.every(function(keyword) { + return Object.prototype.hasOwnProperty.call(parentSchema, keyword); + })) + throw new Error('parent schema must have all required keywords: ' + deps.join(',')); + + var validateSchema = rule.definition.validateSchema; + if (validateSchema) { + var valid = validateSchema(schema); + if (!valid) { + var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors); + if (self._opts.validateSchema == 'log') self.logger.error(message); + else throw new Error(message); + } } } diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index 7d06afab8..66f2aee9b 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -1,6 +1,6 @@ 'use strict'; -var url = require('url') +var URI = require('uri-js') , equal = require('fast-deep-equal') , util = require('./util') , SchemaObject = require('./schema_obj') @@ -67,10 +67,10 @@ function resolve(compile, root, ref) { */ function resolveSchema(root, ref) { /* jshint validthis: true */ - var p = url.parse(ref, false, true) + var p = URI.parse(ref) , refPath = _getFullPath(p) , baseId = getFullPath(this._getId(root.schema)); - if (refPath !== baseId) { + if (Object.keys(root.schema).length === 0 || refPath !== baseId) { var id = normalizeId(refPath); var refVal = this._refs[id]; if (typeof refVal == 'string') { @@ -115,9 +115,9 @@ var PREVENT_SCOPE_CHANGE = util.toHash(['properties', 'patternProperties', 'enum /* @this Ajv */ function getJsonPointer(parsedRef, baseId, schema, root) { /* jshint validthis: true */ - parsedRef.hash = parsedRef.hash || ''; - if (parsedRef.hash.slice(0,2) != '#/') return; - var parts = parsedRef.hash.split('/'); + parsedRef.fragment = parsedRef.fragment || ''; + if (parsedRef.fragment.slice(0,1) != '/') return; + var parts = parsedRef.fragment.split('/'); for (var i = 1; i < parts.length; i++) { var part = parts[i]; @@ -206,14 +206,13 @@ function countKeys(schema) { function getFullPath(id, normalize) { if (normalize !== false) id = normalizeId(id); - var p = url.parse(id, false, true); + var p = URI.parse(id); return _getFullPath(p); } function _getFullPath(p) { - var protocolSeparator = p.protocol || p.href.slice(0,2) == '//' ? '//' : ''; - return (p.protocol||'') + protocolSeparator + (p.host||'') + (p.path||'') + '#'; + return URI.serialize(p).split('#')[0] + '#'; } @@ -225,7 +224,7 @@ function normalizeId(id) { function resolveUrl(baseId, id) { id = normalizeId(id); - return url.resolve(baseId, id); + return URI.resolve(baseId, id); } @@ -246,7 +245,7 @@ function resolveIds(schema) { fullPath += '/' + (typeof keyIndex == 'number' ? keyIndex : util.escapeFragment(keyIndex)); if (typeof id == 'string') { - id = baseId = normalizeId(baseId ? url.resolve(baseId, id) : id); + id = baseId = normalizeId(baseId ? URI.resolve(baseId, id) : id); var refVal = self._refs[id]; if (typeof refVal == 'string') refVal = self._refs[refVal]; diff --git a/lib/compile/rules.js b/lib/compile/rules.js index eaeab77fa..08b25aeb9 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -1,6 +1,6 @@ 'use strict'; -var ruleModules = require('./_rules') +var ruleModules = require('../dotjs') , toHash = require('./util').toHash; module.exports = function rules() { @@ -11,17 +11,20 @@ module.exports = function rules() { { type: 'string', rules: [ 'maxLength', 'minLength', 'pattern', 'format' ] }, { type: 'array', - rules: [ 'maxItems', 'minItems', 'uniqueItems', 'contains', 'items' ] }, + rules: [ 'maxItems', 'minItems', 'items', 'contains', 'uniqueItems' ] }, { type: 'object', rules: [ 'maxProperties', 'minProperties', 'required', 'dependencies', 'propertyNames', { 'properties': ['additionalProperties', 'patternProperties'] } ] }, - { rules: [ '$ref', 'const', 'enum', 'not', 'anyOf', 'oneOf', 'allOf' ] } + { rules: [ '$ref', 'const', 'enum', 'not', 'anyOf', 'oneOf', 'allOf', 'if' ] } ]; - var ALL = [ 'type' ]; + var ALL = [ 'type', '$comment' ]; var KEYWORDS = [ - 'additionalItems', '$schema', 'id', 'title', - 'description', 'default', 'definitions' + '$schema', '$id', 'id', '$data', '$async', 'title', + 'description', 'default', 'definitions', + 'examples', 'readOnly', 'writeOnly', + 'contentMediaType', 'contentEncoding', + 'additionalItems', 'then', 'else' ]; var TYPES = [ 'number', 'integer', 'string', 'array', 'object', 'boolean', 'null' ]; RULES.all = toHash(ALL); @@ -48,6 +51,11 @@ module.exports = function rules() { return rule; }); + RULES.all.$comment = { + keyword: '$comment', + code: ruleModules.$comment + }; + if (group.type) RULES.types[group.type] = group; }); diff --git a/lib/compile/util.js b/lib/compile/util.js index 263891c33..ef07b8c75 100644 --- a/lib/compile/util.js +++ b/lib/compile/util.js @@ -13,10 +13,9 @@ module.exports = { ucs2length: require('./ucs2length'), varOccurences: varOccurences, varReplace: varReplace, - cleanUpCode: cleanUpCode, - finalCleanUpCode: finalCleanUpCode, schemaHasRules: schemaHasRules, schemaHasRulesExcept: schemaHasRulesExcept, + schemaUnknownRules: schemaUnknownRules, toQuotedString: toQuotedString, getPathExpr: getPathExpr, getPath: getPath, @@ -35,7 +34,7 @@ function copy(o, to) { } -function checkDataType(dataType, data, negate) { +function checkDataType(dataType, data, strictNumbers, negate) { var EQUAL = negate ? ' !== ' : ' === ' , AND = negate ? ' || ' : ' && ' , OK = negate ? '!' : '' @@ -48,15 +47,18 @@ function checkDataType(dataType, data, negate) { NOT + 'Array.isArray(' + data + '))'; case 'integer': return '(typeof ' + data + EQUAL + '"number"' + AND + NOT + '(' + data + ' % 1)' + - AND + data + EQUAL + data + ')'; + AND + data + EQUAL + data + + (strictNumbers ? (AND + OK + 'isFinite(' + data + ')') : '') + ')'; + case 'number': return '(typeof ' + data + EQUAL + '"' + dataType + '"' + + (strictNumbers ? (AND + OK + 'isFinite(' + data + ')') : '') + ')'; default: return 'typeof ' + data + EQUAL + '"' + dataType + '"'; } } -function checkDataTypes(dataTypes, data) { +function checkDataTypes(dataTypes, data, strictNumbers) { switch (dataTypes.length) { - case 1: return checkDataType(dataTypes[0], data, true); + case 1: return checkDataType(dataTypes[0], data, strictNumbers, true); default: var code = ''; var types = toHash(dataTypes); @@ -69,7 +71,7 @@ function checkDataTypes(dataTypes, data) { } if (types.number) delete types.integer; for (var t in types) - code += (code ? ' && ' : '' ) + checkDataType(t, data, true); + code += (code ? ' && ' : '' ) + checkDataType(t, data, strictNumbers, true); return code; } @@ -135,42 +137,6 @@ function varReplace(str, dataVar, expr) { } -var EMPTY_ELSE = /else\s*{\s*}/g - , EMPTY_IF_NO_ELSE = /if\s*\([^)]+\)\s*\{\s*\}(?!\s*else)/g - , EMPTY_IF_WITH_ELSE = /if\s*\(([^)]+)\)\s*\{\s*\}\s*else(?!\s*if)/g; -function cleanUpCode(out) { - return out.replace(EMPTY_ELSE, '') - .replace(EMPTY_IF_NO_ELSE, '') - .replace(EMPTY_IF_WITH_ELSE, 'if (!($1))'); -} - - -var ERRORS_REGEXP = /[^v.]errors/g - , REMOVE_ERRORS = /var errors = 0;|var vErrors = null;|validate.errors = vErrors;/g - , REMOVE_ERRORS_ASYNC = /var errors = 0;|var vErrors = null;/g - , RETURN_VALID = 'return errors === 0;' - , RETURN_TRUE = 'validate.errors = null; return true;' - , RETURN_ASYNC = /if \(errors === 0\) return data;\s*else throw new ValidationError\(vErrors\);/ - , RETURN_DATA_ASYNC = 'return data;' - , ROOTDATA_REGEXP = /[^A-Za-z_$]rootData[^A-Za-z0-9_$]/g - , REMOVE_ROOTDATA = /if \(rootData === undefined\) rootData = data;/; - -function finalCleanUpCode(out, async) { - var matches = out.match(ERRORS_REGEXP); - if (matches && matches.length == 2) { - out = async - ? out.replace(REMOVE_ERRORS_ASYNC, '') - .replace(RETURN_ASYNC, RETURN_DATA_ASYNC) - : out.replace(REMOVE_ERRORS, '') - .replace(RETURN_VALID, RETURN_TRUE); - } - - matches = out.match(ROOTDATA_REGEXP); - if (!matches || matches.length !== 3) return out; - return out.replace(REMOVE_ROOTDATA, ''); -} - - function schemaHasRules(schema, rules) { if (typeof schema == 'boolean') return !schema; for (var key in schema) if (rules[key]) return true; @@ -183,6 +149,12 @@ function schemaHasRulesExcept(schema, rules, exceptKeyword) { } +function schemaUnknownRules(schema, rules) { + if (typeof schema == 'boolean') return; + for (var key in schema) if (!rules[key]) return key; +} + + function toQuotedString(str) { return '\'' + escapeQuotes(str) + '\''; } @@ -243,7 +215,7 @@ function getData($data, lvl, paths) { function joinPaths (a, b) { if (a == '""') return b; - return (a + ' + ' + b).replace(/' \+ '/g, ''); + return (a + ' + ' + b).replace(/([^\\])' \+ '/g, '$1'); } diff --git a/lib/$data.js b/lib/data.js similarity index 90% rename from lib/$data.js rename to lib/data.js index 60cfc2d8d..f11142bec 100644 --- a/lib/$data.js +++ b/lib/data.js @@ -38,7 +38,7 @@ module.exports = function (metaSchema, keywordsJsonPointers) { keywords[key] = { anyOf: [ schema, - { $ref: 'https://raw.githubusercontent.com/epoberezkin/ajv/master/lib/refs/$data.json#' } + { $ref: 'https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#' } ] }; } diff --git a/lib/definition_schema.js b/lib/definition_schema.js new file mode 100644 index 000000000..ad86d4f0b --- /dev/null +++ b/lib/definition_schema.js @@ -0,0 +1,37 @@ +'use strict'; + +var metaSchema = require('./refs/json-schema-draft-07.json'); + +module.exports = { + $id: 'https://github.com/ajv-validator/ajv/blob/master/lib/definition_schema.js', + definitions: { + simpleTypes: metaSchema.definitions.simpleTypes + }, + type: 'object', + dependencies: { + schema: ['validate'], + $data: ['validate'], + statements: ['inline'], + valid: {not: {required: ['macro']}} + }, + properties: { + type: metaSchema.properties.type, + schema: {type: 'boolean'}, + statements: {type: 'boolean'}, + dependencies: { + type: 'array', + items: {type: 'string'} + }, + metaSchema: {type: 'object'}, + modifying: {type: 'boolean'}, + valid: {type: 'boolean'}, + $data: {type: 'boolean'}, + async: {type: 'boolean'}, + errors: { + anyOf: [ + {type: 'boolean'}, + {const: 'full'} + ] + } + } +}; diff --git a/lib/dot/_limit.jst b/lib/dot/_limit.jst index 13e7649b3..f15218922 100644 --- a/lib/dot/_limit.jst +++ b/lib/dot/_limit.jst @@ -17,6 +17,15 @@ , $op = $isMax ? '<' : '>' , $notOp = $isMax ? '>' : '<' , $errorKeyword = undefined; + + if (!($isData || typeof $schema == 'number' || $schema === undefined)) { + throw new Error($keyword + ' must be number'); + } + if (!($isDataExcl || $schemaExcl === undefined + || typeof $schemaExcl == 'number' + || typeof $schemaExcl == 'boolean')) { + throw new Error($exclusiveKeyword + ' must be number or boolean'); + } }} {{? $isDataExcl }} @@ -50,6 +59,14 @@ ) || {{=$data}} !== {{=$data}}) { var op{{=$lvl}} = {{=$exclusive}} ? '{{=$op}}' : '{{=$op}}='; + {{ + if ($schema === undefined) { + $errorKeyword = $exclusiveKeyword; + $errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword; + $schemaValue = $schemaValueExcl; + $isData = $isDataExcl; + } + }} {{??}} {{ var $exclIsNumber = typeof $schemaExcl == 'number' diff --git a/lib/dot/_limitItems.jst b/lib/dot/_limitItems.jst index a3e078e51..741329e77 100644 --- a/lib/dot/_limitItems.jst +++ b/lib/dot/_limitItems.jst @@ -3,6 +3,8 @@ {{# def.setupKeyword }} {{# def.$data }} +{{# def.numberKeyword }} + {{ var $op = $keyword == 'maxItems' ? '>' : '<'; }} if ({{# def.$dataNotType:'number' }} {{=$data}}.length {{=$op}} {{=$schemaValue}}) { {{ var $errorKeyword = $keyword; }} diff --git a/lib/dot/_limitLength.jst b/lib/dot/_limitLength.jst index cfc8dbb01..285c66bd2 100644 --- a/lib/dot/_limitLength.jst +++ b/lib/dot/_limitLength.jst @@ -3,6 +3,8 @@ {{# def.setupKeyword }} {{# def.$data }} +{{# def.numberKeyword }} + {{ var $op = $keyword == 'maxLength' ? '>' : '<'; }} if ({{# def.$dataNotType:'number' }} {{# def.strLength }} {{=$op}} {{=$schemaValue}}) { {{ var $errorKeyword = $keyword; }} diff --git a/lib/dot/_limitProperties.jst b/lib/dot/_limitProperties.jst index da7ea776f..c4c21551a 100644 --- a/lib/dot/_limitProperties.jst +++ b/lib/dot/_limitProperties.jst @@ -3,6 +3,8 @@ {{# def.setupKeyword }} {{# def.$data }} +{{# def.numberKeyword }} + {{ var $op = $keyword == 'maxProperties' ? '>' : '<'; }} if ({{# def.$dataNotType:'number' }} Object.keys({{=$data}}).length {{=$op}} {{=$schemaValue}}) { {{ var $errorKeyword = $keyword; }} diff --git a/lib/dot/allOf.jst b/lib/dot/allOf.jst index 4c2836311..0e782fe98 100644 --- a/lib/dot/allOf.jst +++ b/lib/dot/allOf.jst @@ -30,5 +30,3 @@ {{= $closingBraces.slice(0,-1) }} {{?}} {{?}} - -{{# def.cleanUp }} diff --git a/lib/dot/anyOf.jst b/lib/dot/anyOf.jst index 086cf2b33..ea909ee62 100644 --- a/lib/dot/anyOf.jst +++ b/lib/dot/anyOf.jst @@ -39,8 +39,6 @@ } else { {{# def.resetErrors }} {{? it.opts.allErrors }} } {{?}} - - {{# def.cleanUp }} {{??}} {{? $breakOnError }} if (true) { diff --git a/lib/dot/comment.jst b/lib/dot/comment.jst new file mode 100644 index 000000000..f95915035 --- /dev/null +++ b/lib/dot/comment.jst @@ -0,0 +1,9 @@ +{{# def.definitions }} +{{# def.setupKeyword }} + +{{ var $comment = it.util.toQuotedString($schema); }} +{{? it.opts.$comment === true }} + console.log({{=$comment}}); +{{?? typeof it.opts.$comment == 'function' }} + self._opts.$comment({{=$comment}}, {{=it.util.toQuotedString($errSchemaPath)}}, validate.root.schema); +{{?}} diff --git a/lib/dot/contains.jst b/lib/dot/contains.jst index 925d2c84b..4dc996741 100644 --- a/lib/dot/contains.jst +++ b/lib/dot/contains.jst @@ -53,5 +53,3 @@ var {{=$valid}}; {{# def.resetErrors }} {{?}} {{? it.opts.allErrors }} } {{?}} - -{{# def.cleanUp }} diff --git a/lib/dot/custom.jst b/lib/dot/custom.jst index 402028e6b..d30588fb0 100644 --- a/lib/dot/custom.jst +++ b/lib/dot/custom.jst @@ -112,13 +112,13 @@ var {{=$valid}}; {{# def.storeDefOut:def_callRuleValidate }} {{? $rDef.errors === false }} - {{=$valid}} = {{? $asyncKeyword }}{{=it.yieldAwait}}{{?}}{{= def_callRuleValidate }}; + {{=$valid}} = {{? $asyncKeyword }}await {{?}}{{= def_callRuleValidate }}; {{??}} {{? $asyncKeyword }} {{ $ruleErrs = 'customErrors' + $lvl; }} var {{=$ruleErrs}} = null; try { - {{=$valid}} = {{=it.yieldAwait}}{{= def_callRuleValidate }}; + {{=$valid}} = await {{= def_callRuleValidate }}; } catch (e) { {{=$valid}} = false; if (e instanceof ValidationError) {{=$ruleErrs}} = e.errors; diff --git a/lib/dot/defaults.def b/lib/dot/defaults.def index 5ad8d1d2d..a844cf285 100644 --- a/lib/dot/defaults.def +++ b/lib/dot/defaults.def @@ -1,10 +1,25 @@ {{## def.assignDefault: - if ({{=$passData}} === undefined) - {{=$passData}} = {{? it.opts.useDefaults == 'shared' }} - {{= it.useDefault($sch.default) }} - {{??}} - {{= JSON.stringify($sch.default) }} - {{?}}; + {{? it.compositeRule }} + {{ + if (it.opts.strictDefaults) { + var $defaultMsg = 'default is ignored for: ' + $passData; + if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); + else throw new Error($defaultMsg); + } + }} + {{??}} + if ({{=$passData}} === undefined + {{? it.opts.useDefaults == 'empty' }} + || {{=$passData}} === null + || {{=$passData}} === '' + {{?}} + ) + {{=$passData}} = {{? it.opts.useDefaults == 'shared' }} + {{= it.useDefault($sch.default) }} + {{??}} + {{= JSON.stringify($sch.default) }} + {{?}}; + {{?}} #}} diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index cdbe140bb..db4ea6f32 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -63,7 +63,9 @@ {{## def.nonEmptySchema:_schema: - it.util.schemaHasRules(_schema, it.RULES.all) + (it.opts.strictKeywords + ? typeof _schema == 'object' && Object.keys(_schema).length > 0 + : it.util.schemaHasRules(_schema, it.RULES.all)) #}} @@ -110,12 +112,6 @@ #}} -{{## def.cleanUp: {{ out = it.util.cleanUpCode(out); }} #}} - - -{{## def.finalCleanUp: {{ out = it.util.finalCleanUpCode(out, $async); }} #}} - - {{## def.$data: {{ var $isData = it.opts.$data && $schema && $schema.$data @@ -142,6 +138,13 @@ #}} +{{## def.numberKeyword: + {{? !($isData || typeof $schema == 'number') }} + {{ throw new Error($keyword + ' must be number'); }} + {{?}} +#}} + + {{## def.beginDefOut: {{ var $$outStack = $$outStack || []; diff --git a/lib/dot/dependencies.jst b/lib/dot/dependencies.jst index c41f33422..e4bdddec8 100644 --- a/lib/dot/dependencies.jst +++ b/lib/dot/dependencies.jst @@ -19,6 +19,7 @@ , $ownProperties = it.opts.ownProperties; for ($property in $schema) { + if ($property == '__proto__') continue; var $sch = $schema[$property]; var $deps = Array.isArray($sch) ? $propertyDeps : $schemaDeps; $deps[$property] = $sch; @@ -76,5 +77,3 @@ var missing{{=$lvl}}; {{= $closingBraces }} if ({{=$errs}} == errors) { {{?}} - -{{# def.cleanUp }} diff --git a/lib/dot/errors.def b/lib/dot/errors.def index b79646fc2..5c5752cb0 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -94,23 +94,23 @@ 'false schema': "'boolean schema is false'", $ref: "'can\\\'t resolve reference {{=it.util.escapeQuotes($schema)}}'", additionalItems: "'should NOT have more than {{=$schema.length}} items'", - additionalProperties: "'should NOT have additional properties'", + additionalProperties: "'{{? it.opts._errorDataPathProperty }}is an invalid additional property{{??}}should NOT have additional properties{{?}}'", anyOf: "'should match some schema in anyOf'", const: "'should be equal to constant'", contains: "'should contain a valid item'", dependencies: "'should have {{? $deps.length == 1 }}property {{= it.util.escapeQuotes($deps[0]) }}{{??}}properties {{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}} when property {{= it.util.escapeQuotes($property) }} is present'", 'enum': "'should be equal to one of the allowed values'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", + 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", _limit: "'should be {{=$opStr}} {{#def.appendSchema}}", _exclusiveLimit: "'{{=$exclusiveKeyword}} should be boolean'", - _limitItems: "'should NOT have {{?$keyword=='maxItems'}}more{{??}}less{{?}} than {{#def.concatSchema}} items'", + _limitItems: "'should NOT have {{?$keyword=='maxItems'}}more{{??}}fewer{{?}} than {{#def.concatSchema}} items'", _limitLength: "'should NOT be {{?$keyword=='maxLength'}}longer{{??}}shorter{{?}} than {{#def.concatSchema}} characters'", - _limitProperties:"'should NOT have {{?$keyword=='maxProperties'}}more{{??}}less{{?}} than {{#def.concatSchema}} properties'", + _limitProperties:"'should NOT have {{?$keyword=='maxProperties'}}more{{??}}fewer{{?}} than {{#def.concatSchema}} properties'", multipleOf: "'should be multiple of {{#def.appendSchema}}", not: "'should NOT be valid'", oneOf: "'should match exactly one schema in oneOf'", pattern: "'should match pattern \"{{#def.concatSchemaEQ}}\"'", - patternGroups: "'should NOT have {{=$moreOrLess}} than {{=$limit}} properties matching pattern \"{{=it.util.escapeQuotes($pgProperty)}}\"'", propertyNames: "'property name \\'{{=$invalidName}}\\' is invalid'", required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property \\'{{=$missingProperty}}\\'{{?}}'", type: "'should be {{? $typeIsArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}'", @@ -137,6 +137,7 @@ dependencies: "validate.schema{{=$schemaPath}}", 'enum': "validate.schema{{=$schemaPath}}", format: "{{#def.schemaRefOrQS}}", + 'if': "validate.schema{{=$schemaPath}}", _limit: "{{#def.schemaRefOrVal}}", _exclusiveLimit: "validate.schema{{=$schemaPath}}", _limitItems: "{{#def.schemaRefOrVal}}", @@ -146,7 +147,6 @@ not: "validate.schema{{=$schemaPath}}", oneOf: "validate.schema{{=$schemaPath}}", pattern: "{{#def.schemaRefOrQS}}", - patternGroups: "validate.schema{{=$schemaPath}}", propertyNames: "validate.schema{{=$schemaPath}}", required: "validate.schema{{=$schemaPath}}", type: "validate.schema{{=$schemaPath}}", @@ -167,11 +167,12 @@ additionalItems: "{ limit: {{=$schema.length}} }", additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", anyOf: "{}", - const: "{}", + const: "{ allowedValue: schema{{=$lvl}} }", contains: "{}", dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{= it.util.escapeQuotes($deps.length==1 ? $deps[0] : $deps.join(\", \")) }}' }", 'enum': "{ allowedValues: schema{{=$lvl}} }", format: "{ format: {{#def.schemaValueQS}} }", + 'if': "{ failingKeyword: {{=$ifClause}} }", _limit: "{ comparison: {{=$opExpr}}, limit: {{=$schemaValue}}, exclusive: {{=$exclusive}} }", _exclusiveLimit: "{}", _limitItems: "{ limit: {{=$schemaValue}} }", @@ -179,9 +180,8 @@ _limitProperties:"{ limit: {{=$schemaValue}} }", multipleOf: "{ multipleOf: {{=$schemaValue}} }", not: "{}", - oneOf: "{}", + oneOf: "{ passingSchemas: {{=$passingSchemas}} }", pattern: "{ pattern: {{#def.schemaValueQS}} }", - patternGroups: "{ reason: '{{=$reason}}', limit: {{=$limit}}, pattern: '{{=it.util.escapeQuotes($pgProperty)}}' }", propertyNames: "{ propertyName: '{{=$invalidName}}' }", required: "{ missingProperty: '{{=$missingProperty}}' }", type: "{ type: '{{? $typeIsArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }", diff --git a/lib/dot/format.jst b/lib/dot/format.jst index 074d16c31..37f14da80 100644 --- a/lib/dot/format.jst +++ b/lib/dot/format.jst @@ -24,7 +24,7 @@ ({{=$format}} && {{=$formatType}} == '{{=$ruleType}}' && !(typeof {{=$format}} == 'function' ? {{? it.async}} - (async{{=$lvl}} ? {{=it.yieldAwait}} {{=$format}}({{=$data}}) : {{=$format}}({{=$data}})) + (async{{=$lvl}} ? await {{=$format}}({{=$data}}) : {{=$format}}({{=$data}})) {{??}} {{=$format}}({{=$data}}) {{?}} @@ -71,7 +71,7 @@ {{ var $format = it.formats[$schema]; }} {{? !$format }} {{? $unknownFormats == 'ignore' }} - {{ console.warn('unknown format "' + $schema + '" ignored in schema at path "' + it.errSchemaPath + '"'); }} + {{ it.logger.warn('unknown format "' + $schema + '" ignored in schema at path "' + it.errSchemaPath + '"'); }} {{# def.skipFormat }} {{?? $allowUnknown && $unknownFormats.indexOf($schema) >= 0 }} {{# def.skipFormat }} @@ -97,7 +97,7 @@ if (!it.async) throw new Error('async format in sync schema'); var $formatRef = 'formats' + it.util.getProperty($schema) + '.validate'; }} - if (!({{=it.yieldAwait}} {{=$formatRef}}({{=$data}}))) { + if (!(await {{=$formatRef}}({{=$data}}))) { {{??}} if (!{{# def.checkFormat }}) { {{?}} diff --git a/lib/dot/if.jst b/lib/dot/if.jst new file mode 100644 index 000000000..adb503612 --- /dev/null +++ b/lib/dot/if.jst @@ -0,0 +1,73 @@ +{{# def.definitions }} +{{# def.errors }} +{{# def.setupKeyword }} +{{# def.setupNextLevel }} + + +{{## def.validateIfClause:_clause: + {{ + $it.schema = it.schema['_clause']; + $it.schemaPath = it.schemaPath + '._clause'; + $it.errSchemaPath = it.errSchemaPath + '/_clause'; + }} + {{# def.insertSubschemaCode }} + {{=$valid}} = {{=$nextValid}}; + {{? $thenPresent && $elsePresent }} + {{ $ifClause = 'ifClause' + $lvl; }} + var {{=$ifClause}} = '_clause'; + {{??}} + {{ $ifClause = '\'_clause\''; }} + {{?}} +#}} + +{{ + var $thenSch = it.schema['then'] + , $elseSch = it.schema['else'] + , $thenPresent = $thenSch !== undefined && {{# def.nonEmptySchema:$thenSch }} + , $elsePresent = $elseSch !== undefined && {{# def.nonEmptySchema:$elseSch }} + , $currentBaseId = $it.baseId; +}} + +{{? $thenPresent || $elsePresent }} + {{ + var $ifClause; + $it.createErrors = false; + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + }} + var {{=$errs}} = errors; + var {{=$valid}} = true; + + {{# def.setCompositeRule }} + {{# def.insertSubschemaCode }} + {{ $it.createErrors = true; }} + {{# def.resetErrors }} + {{# def.resetCompositeRule }} + + {{? $thenPresent }} + if ({{=$nextValid}}) { + {{# def.validateIfClause:then }} + } + {{? $elsePresent }} + else { + {{?}} + {{??}} + if (!{{=$nextValid}}) { + {{?}} + + {{? $elsePresent }} + {{# def.validateIfClause:else }} + } + {{?}} + + if (!{{=$valid}}) { + {{# def.extraError:'if' }} + } + {{? $breakOnError }} else { {{?}} +{{??}} + {{? $breakOnError }} + if (true) { + {{?}} +{{?}} + diff --git a/lib/dot/items.jst b/lib/dot/items.jst index 8c0f5acb5..acc932a26 100644 --- a/lib/dot/items.jst +++ b/lib/dot/items.jst @@ -96,5 +96,3 @@ var {{=$valid}}; {{= $closingBraces }} if ({{=$errs}} == errors) { {{?}} - -{{# def.cleanUp }} diff --git a/lib/dot/multipleOf.jst b/lib/dot/multipleOf.jst index 5f8dd33b5..6d88a456f 100644 --- a/lib/dot/multipleOf.jst +++ b/lib/dot/multipleOf.jst @@ -3,6 +3,8 @@ {{# def.setupKeyword }} {{# def.$data }} +{{# def.numberKeyword }} + var division{{=$lvl}}; if ({{?$isData}} {{=$schemaValue}} !== undefined && ( diff --git a/lib/dot/oneOf.jst b/lib/dot/oneOf.jst index 59a435549..bcce2c6ed 100644 --- a/lib/dot/oneOf.jst +++ b/lib/dot/oneOf.jst @@ -3,11 +3,17 @@ {{# def.setupKeyword }} {{# def.setupNextLevel }} -var {{=$errs}} = errors; -var prevValid{{=$lvl}} = false; -var {{=$valid}} = false; +{{ + var $currentBaseId = $it.baseId + , $prevValid = 'prevValid' + $lvl + , $passingSchemas = 'passingSchemas' + $lvl; +}} + +var {{=$errs}} = errors + , {{=$prevValid}} = false + , {{=$valid}} = false + , {{=$passingSchemas}} = null; -{{ var $currentBaseId = $it.baseId; }} {{# def.setCompositeRule }} {{~ $schema:$sch:$i }} @@ -24,13 +30,17 @@ var {{=$valid}} = false; {{?}} {{? $i }} - if ({{=$nextValid}} && prevValid{{=$lvl}}) + if ({{=$nextValid}} && {{=$prevValid}}) { {{=$valid}} = false; - else { + {{=$passingSchemas}} = [{{=$passingSchemas}}, {{=$i}}]; + } else { {{ $closingBraces += '}'; }} {{?}} - if ({{=$nextValid}}) {{=$valid}} = prevValid{{=$lvl}} = true; + if ({{=$nextValid}}) { + {{=$valid}} = {{=$prevValid}} = true; + {{=$passingSchemas}} = {{=$i}}; + } {{~}} {{# def.resetCompositeRule }} diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index 8d56324b7..5cebb9b12 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -28,9 +28,9 @@ , $nextData = 'data' + $dataNxt , $dataProperties = 'dataProperties' + $lvl; - var $schemaKeys = Object.keys($schema || {}) + var $schemaKeys = Object.keys($schema || {}).filter(notProto) , $pProperties = it.schema.patternProperties || {} - , $pPropertyKeys = Object.keys($pProperties) + , $pPropertyKeys = Object.keys($pProperties).filter(notProto) , $aProperties = it.schema.additionalProperties , $someProperties = $schemaKeys.length || $pPropertyKeys.length , $noAdditional = $aProperties === false @@ -42,13 +42,11 @@ , $currentBaseId = it.baseId; var $required = it.schema.required; - if ($required && !(it.opts.v5 && $required.$data) && $required.length < it.opts.loopRequired) + if ($required && !(it.opts.$data && $required.$data) && $required.length < it.opts.loopRequired) { var $requiredHash = it.util.toHash($required); - - if (it.opts.patternGroups) { - var $pgProperties = it.schema.patternGroups || {} - , $pgPropertyKeys = Object.keys($pgProperties); } + + function notProto(p) { return p !== '__proto__'; } }} @@ -63,8 +61,8 @@ var {{=$nextValid}} = true; {{? $someProperties }} var isAdditional{{=$lvl}} = !(false {{? $schemaKeys.length }} - {{? $schemaKeys.length > 5 }} - || validate.schema{{=$schemaPath}}[{{=$key}}] + {{? $schemaKeys.length > 8 }} + || validate.schema{{=$schemaPath}}.hasOwnProperty({{=$key}}) {{??}} {{~ $schemaKeys:$propertyKey }} || {{=$key}} == {{= it.util.toQuotedString($propertyKey) }} @@ -76,11 +74,6 @@ var {{=$nextValid}} = true; || {{= it.usePattern($pProperty) }}.test({{=$key}}) {{~}} {{?}} - {{? it.opts.patternGroups && $pgPropertyKeys.length }} - {{~ $pgPropertyKeys:$pgProperty:$i }} - || {{= it.usePattern($pgProperty) }}.test({{=$key}}) - {{~}} - {{?}} ); if (isAdditional{{=$lvl}}) { @@ -246,82 +239,7 @@ var {{=$nextValid}} = true; {{?}} -{{? it.opts.patternGroups && $pgPropertyKeys.length }} - {{~ $pgPropertyKeys:$pgProperty }} - {{ - var $pgSchema = $pgProperties[$pgProperty] - , $sch = $pgSchema.schema; - }} - - {{? {{# def.nonEmptySchema:$sch}} }} - {{ - $it.schema = $sch; - $it.schemaPath = it.schemaPath + '.patternGroups' + it.util.getProperty($pgProperty) + '.schema'; - $it.errSchemaPath = it.errSchemaPath + '/patternGroups/' - + it.util.escapeFragment($pgProperty) - + '/schema'; - }} - - var pgPropCount{{=$lvl}} = 0; - - {{# def.iterateProperties }} - if ({{= it.usePattern($pgProperty) }}.test({{=$key}})) { - pgPropCount{{=$lvl}}++; - - {{ - $it.errorPath = it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); - var $passData = $data + '[' + $key + ']'; - $it.dataPathArr[$dataNxt] = $key; - }} - - {{# def.generateSubschemaCode }} - {{# def.optimizeValidate }} - - {{? $breakOnError }} if (!{{=$nextValid}}) break; {{?}} - } - {{? $breakOnError }} else {{=$nextValid}} = true; {{?}} - } - - {{# def.ifResultValid }} - - {{ - var $pgMin = $pgSchema.minimum - , $pgMax = $pgSchema.maximum; - }} - {{? $pgMin !== undefined || $pgMax !== undefined }} - var {{=$valid}} = true; - - {{ var $currErrSchemaPath = $errSchemaPath; }} - - {{? $pgMin !== undefined }} - {{ var $limit = $pgMin, $reason = 'minimum', $moreOrLess = 'less'; }} - {{=$valid}} = pgPropCount{{=$lvl}} >= {{=$pgMin}}; - {{ $errSchemaPath = it.errSchemaPath + '/patternGroups/minimum'; }} - {{# def.checkError:'patternGroups' }} - {{? $pgMax !== undefined }} - else - {{?}} - {{?}} - - {{? $pgMax !== undefined }} - {{ var $limit = $pgMax, $reason = 'maximum', $moreOrLess = 'more'; }} - {{=$valid}} = pgPropCount{{=$lvl}} <= {{=$pgMax}}; - {{ $errSchemaPath = it.errSchemaPath + '/patternGroups/maximum'; }} - {{# def.checkError:'patternGroups' }} - {{?}} - - {{ $errSchemaPath = $currErrSchemaPath; }} - - {{# def.ifValid }} - {{?}} - {{?}} {{ /* def.nonEmptySchema */ }} - {{~}} -{{?}} - - {{? $breakOnError }} {{= $closingBraces }} if ({{=$errs}} == errors) { {{?}} - -{{# def.cleanUp }} diff --git a/lib/dot/propertyNames.jst b/lib/dot/propertyNames.jst index 51caffc20..d456ccafc 100644 --- a/lib/dot/propertyNames.jst +++ b/lib/dot/propertyNames.jst @@ -3,6 +3,8 @@ {{# def.setupKeyword }} {{# def.setupNextLevel }} +var {{=$errs}} = errors; + {{? {{# def.nonEmptySchema:$schema }} }} {{ $it.schema = $schema; @@ -22,8 +24,6 @@ , $currentBaseId = it.baseId; }} - var {{=$errs}} = errors; - {{? $ownProperties }} var {{=$dataProperties}} = undefined; {{?}} @@ -50,5 +50,3 @@ {{= $closingBraces }} if ({{=$errs}} == errors) { {{?}} - -{{# def.cleanUp }} diff --git a/lib/dot/ref.jst b/lib/dot/ref.jst index 4a0889686..253e3507c 100644 --- a/lib/dot/ref.jst +++ b/lib/dot/ref.jst @@ -27,11 +27,11 @@ {{? $refVal === undefined }} {{ var $message = it.MissingRefError.message(it.baseId, $schema); }} {{? it.opts.missingRefs == 'fail' }} - {{ console.error($message); }} + {{ it.logger.error($message); }} {{# def.error:'$ref' }} {{? $breakOnError }} if (false) { {{?}} {{?? it.opts.missingRefs == 'ignore' }} - {{ console.warn($message); }} + {{ it.logger.warn($message); }} {{? $breakOnError }} if (true) { {{?}} {{??}} {{ throw new it.MissingRefError(it.baseId, $schema, $message); }} @@ -50,7 +50,7 @@ {{?}} {{??}} {{ - $async = $refVal.$async === true; + $async = $refVal.$async === true || (it.async && $refVal.$async !== false); $refCode = $refVal.code; }} {{?}} @@ -65,7 +65,7 @@ {{ if (!it.async) throw new Error('async schema referenced by sync schema'); }} {{? $breakOnError }} var {{=$valid}}; {{?}} try { - {{=it.yieldAwait}} {{=__callValidate}}; + await {{=__callValidate}}; {{? $breakOnError }} {{=$valid}} = true; {{?}} } catch (e) { if (!(e instanceof ValidationError)) throw e; diff --git a/lib/dot/uniqueItems.jst b/lib/dot/uniqueItems.jst index dfc42b03b..e69b8308d 100644 --- a/lib/dot/uniqueItems.jst +++ b/lib/dot/uniqueItems.jst @@ -14,18 +14,42 @@ else { {{?}} - var {{=$valid}} = true; - if ({{=$data}}.length > 1) { - var i = {{=$data}}.length, j; - outer: - for (;i--;) { - for (j = i; j--;) { - if (equal({{=$data}}[i], {{=$data}}[j])) { + var i = {{=$data}}.length + , {{=$valid}} = true + , j; + if (i > 1) { + {{ + var $itemType = it.schema.items && it.schema.items.type + , $typeIsArray = Array.isArray($itemType); + }} + {{? !$itemType || $itemType == 'object' || $itemType == 'array' || + ($typeIsArray && ($itemType.indexOf('object') >= 0 || $itemType.indexOf('array') >= 0)) }} + outer: + for (;i--;) { + for (j = i; j--;) { + if (equal({{=$data}}[i], {{=$data}}[j])) { + {{=$valid}} = false; + break outer; + } + } + } + {{??}} + var itemIndices = {}, item; + for (;i--;) { + var item = {{=$data}}[i]; + {{ var $method = 'checkDataType' + ($typeIsArray ? 's' : ''); }} + if ({{= it.util[$method]($itemType, 'item', it.opts.strictNumbers, true) }}) continue; + {{? $typeIsArray}} + if (typeof item == 'string') item = '"' + item; + {{?}} + if (typeof itemIndices[item] == 'number') { {{=$valid}} = false; - break outer; + j = itemIndices[item]; + break; } + itemIndices[item] = i; } - } + {{?}} } {{? $isData }} } {{?}} diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index 4ebc599c0..fd833a535 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -20,30 +20,23 @@ , $id = it.self._getId(it.schema); }} -{{? it.isTop }} - {{? $async }} - {{ - it.async = true; - var $es7 = it.opts.async == 'es7'; - it.yieldAwait = $es7 ? 'await' : 'yield'; - }} - {{?}} +{{ + if (it.opts.strictKeywords) { + var $unknownKwd = it.util.schemaUnknownRules(it.schema, it.RULES.keywords); + if ($unknownKwd) { + var $keywordsMsg = 'unknown keyword: ' + $unknownKwd; + if (it.opts.strictKeywords === 'log') it.logger.warn($keywordsMsg); + else throw new Error($keywordsMsg); + } + } +}} - var validate = - {{? $async }} - {{? $es7 }} - (async function - {{??}} - {{? it.opts.async != '*'}}co.wrap{{?}}(function* - {{?}} - {{??}} - (function +{{? it.isTop }} + var validate = {{?$async}}{{it.async = true;}}async {{?}}function(data, dataPath, parentData, parentDataProperty, rootData) { + 'use strict'; + {{? $id && (it.opts.sourceCode || it.opts.processCode) }} + {{= '/\*# sourceURL=' + $id + ' */' }} {{?}} - (data, dataPath, parentData, parentDataProperty, rootData) { - 'use strict'; - {{? $id && (it.opts.sourceCode || it.opts.processCode) }} - {{= '/\*# sourceURL=' + $id + ' */' }} - {{?}} {{?}} {{? typeof it.schema == 'boolean' || !($refKeywords || it.schema.$ref) }} @@ -70,7 +63,7 @@ {{?}} {{? it.isTop}} - }); + }; return validate; {{?}} @@ -89,6 +82,12 @@ delete it.isTop; it.dataPathArr = [undefined]; + + if (it.schema.default !== undefined && it.opts.useDefaults && it.opts.strictDefaults) { + var $defaultMsg = 'default is ignored in the schema root'; + if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); + else throw new Error($defaultMsg); + } }} var vErrors = null; {{ /* don't edit, used in replace */ }} @@ -118,6 +117,16 @@ var $typeSchema = it.schema.type , $typeIsArray = Array.isArray($typeSchema); + if ($typeSchema && it.opts.nullable && it.schema.nullable === true) { + if ($typeIsArray) { + if ($typeSchema.indexOf('null') == -1) + $typeSchema = $typeSchema.concat('null'); + } else if ($typeSchema != 'null') { + $typeSchema = [$typeSchema, 'null']; + $typeIsArray = true; + } + } + if ($typeIsArray && $typeSchema.length == 1) { $typeSchema = $typeSchema[0]; $typeIsArray = false; @@ -131,7 +140,7 @@ , $method = $typeIsArray ? 'checkDataTypes' : 'checkDataType'; }} - if ({{= it.util[$method]($typeSchema, $data, true) }}) { + if ({{= it.util[$method]($typeSchema, $data, it.opts.strictNumbers, true) }}) { #}} {{? it.schema.$ref && $refKeywords }} @@ -140,11 +149,15 @@ {{?? it.opts.extendRefs !== true }} {{ $refKeywords = false; - console.warn('$ref: keywords ignored in schema at path "' + it.errSchemaPath + '"'); + it.logger.warn('$ref: keywords ignored in schema at path "' + it.errSchemaPath + '"'); }} {{?}} {{?}} +{{? it.schema.$comment && it.opts.$comment }} + {{= it.RULES.all.$comment.code(it, '$comment') }} +{{?}} + {{? $typeSchema }} {{? it.opts.coerceTypes }} {{ var $coerceToTypes = it.util.coerceToTypes(it.opts.coerceTypes, $typeSchema); }} @@ -176,15 +189,12 @@ {{ $closingBraces2 += '}'; }} {{?}} {{??}} - {{? it.opts.v5 && it.schema.patternGroups }} - {{ console.warn('keyword "patternGroups" is deprecated and disabled. Use option patternGroups: true to enable.'); }} - {{?}} {{~ it.RULES:$rulesGroup }} {{? $shouldUseGroup($rulesGroup) }} {{? $rulesGroup.type }} - if ({{= it.util.checkDataType($rulesGroup.type, $data) }}) { + if ({{= it.util.checkDataType($rulesGroup.type, $data, it.opts.strictNumbers) }}) { {{?}} - {{? it.opts.useDefaults && !it.compositeRule }} + {{? it.opts.useDefaults }} {{? $rulesGroup.type == 'object' && it.schema.properties }} {{# def.defaultProperties }} {{?? $rulesGroup.type == 'array' && Array.isArray(it.schema.items) }} @@ -237,19 +247,13 @@ validate.errors = vErrors; {{ /* don't edit, used in replace */ }} return errors === 0; {{ /* don't edit, used in replace */ }} {{?}} - }); + }; return validate; {{??}} var {{=$valid}} = errors === errs_{{=$lvl}}; {{?}} -{{# def.cleanUp }} - -{{? $top }} - {{# def.finalCleanUp }} -{{?}} - {{ function $shouldUseGroup($rulesGroup) { var rules = $rulesGroup.rules; @@ -260,10 +264,10 @@ function $shouldUseRule($rule) { return it.schema[$rule.keyword] !== undefined || - ($rule.implements && $ruleImlementsSomeKeyword($rule)); + ($rule.implements && $ruleImplementsSomeKeyword($rule)); } - function $ruleImlementsSomeKeyword($rule) { + function $ruleImplementsSomeKeyword($rule) { var impl = $rule.implements; for (var i=0; i < impl.length; i++) if (it.schema[impl[i]] !== undefined) diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js new file mode 100644 index 000000000..2fb1b00ef --- /dev/null +++ b/lib/dotjs/index.js @@ -0,0 +1,33 @@ +'use strict'; + +//all requires must be explicit because browserify won't work with dynamic requires +module.exports = { + '$ref': require('./ref'), + allOf: require('./allOf'), + anyOf: require('./anyOf'), + '$comment': require('./comment'), + const: require('./const'), + contains: require('./contains'), + dependencies: require('./dependencies'), + 'enum': require('./enum'), + format: require('./format'), + 'if': require('./if'), + items: require('./items'), + maximum: require('./_limit'), + minimum: require('./_limit'), + maxItems: require('./_limitItems'), + minItems: require('./_limitItems'), + maxLength: require('./_limitLength'), + minLength: require('./_limitLength'), + maxProperties: require('./_limitProperties'), + minProperties: require('./_limitProperties'), + multipleOf: require('./multipleOf'), + not: require('./not'), + oneOf: require('./oneOf'), + pattern: require('./pattern'), + properties: require('./properties'), + propertyNames: require('./propertyNames'), + required: require('./required'), + uniqueItems: require('./uniqueItems'), + validate: require('./validate') +}; diff --git a/lib/keyword.js b/lib/keyword.js index 85e64c600..06da9a2df 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -2,24 +2,27 @@ var IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i; var customRuleCode = require('./dotjs/custom'); +var definitionSchema = require('./definition_schema'); module.exports = { add: addKeyword, get: getKeyword, - remove: removeKeyword + remove: removeKeyword, + validate: validateKeyword }; + /** * Define custom keyword * @this Ajv * @param {String} keyword custom keyword, should be unique (including different from all standard, custom and macro keywords). * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. + * @return {Ajv} this for method chaining */ function addKeyword(keyword, definition) { /* jshint validthis: true */ /* eslint no-shadow: 0 */ var RULES = this.RULES; - if (RULES.keywords[keyword]) throw new Error('Keyword ' + keyword + ' is already defined'); @@ -27,30 +30,23 @@ function addKeyword(keyword, definition) { throw new Error('Keyword ' + keyword + ' is not a valid identifier'); if (definition) { - if (definition.macro && definition.valid !== undefined) - throw new Error('"valid" option cannot be used with macro keywords'); + this.validateKeyword(definition, true); var dataType = definition.type; if (Array.isArray(dataType)) { - var i, len = dataType.length; - for (i=0; i=4\" eslint lib/*.js lib/compile/*.js spec scripts", - "jshint": "jshint lib/*.js lib/**/*.js --exclude lib/dotjs/**/*", - "test-spec": "mocha spec/*.spec.js -R spec $(if-node-version 7 echo --harmony-async-await)", + "eslint": "eslint lib/{compile/,}*.js spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", + "jshint": "jshint lib/{compile/,}*.js", + "lint": "npm run jshint && npm run eslint", + "test-spec": "mocha spec/{**/,}*.spec.js -R spec", "test-fast": "AJV_FAST_TEST=true npm run test-spec", - "test-debug": "mocha spec/*.spec.js --debug-brk -R spec", + "test-debug": "npm run test-spec -- --inspect-brk", "test-cov": "nyc npm run test-spec", - "test-ts": "tsc --target ES5 --noImplicitAny lib/ajv.d.ts", - "bundle": "node ./scripts/bundle.js . Ajv pure_getters", - "bundle-regenerator": "node ./scripts/bundle.js regenerator", - "bundle-nodent": "node ./scripts/bundle.js nodent", - "bundle-all": "del-cli dist && npm run bundle && npm run bundle-regenerator && npm run bundle-nodent", + "test-ts": "tsc --target ES5 --noImplicitAny --noEmit spec/typescript/index.ts", + "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", "bundle-beautify": "node ./scripts/bundle.js js-beautify", - "build": "del-cli lib/dotjs/*.js && node scripts/compile-dots.js", - "test-karma": "karma start --single-run --browsers PhantomJS", - "test-browser": "del-cli .browser && npm run bundle-all && scripts/prepare-tests && npm run test-karma", - "test": "npm run jshint && npm run eslint && npm run test-ts && npm run build && npm run test-cov && if-node-version 4 npm run test-browser", - "prepublish": "npm run build && npm run bundle-all", - "watch": "watch 'npm run build' ./lib/dot" + "build": "del-cli lib/dotjs/*.js \"!lib/dotjs/index.js\" && node scripts/compile-dots.js", + "test-karma": "karma start", + "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", + "test-all": "npm run test-cov && if-node-version 10 npm run test-browser", + "test": "npm run lint && npm run build && npm run test-all", + "prepublish": "npm run build && npm run bundle", + "watch": "watch \"npm run build\" ./lib/dot" }, "nyc": { "exclude": [ @@ -43,7 +42,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/epoberezkin/ajv.git" + "url": "https://github.com/ajv-validator/ajv.git" }, "keywords": [ "JSON", @@ -58,46 +57,50 @@ "author": "Evgeny Poberezkin", "license": "MIT", "bugs": { - "url": "https://github.com/epoberezkin/ajv/issues" + "url": "https://github.com/ajv-validator/ajv/issues" }, - "homepage": "https://github.com/epoberezkin/ajv", + "homepage": "https://github.com/ajv-validator/ajv", "tonicExampleFilename": ".tonic_example.js", "dependencies": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "json-schema-traverse": "^0.3.0", - "json-stable-stringify": "^1.0.1" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "devDependencies": { - "ajv-async": "^0.1.0", - "bluebird": "^3.1.5", - "brfs": "^1.4.3", - "browserify": "^14.1.0", + "ajv-async": "^1.0.0", + "bluebird": "^3.5.3", + "brfs": "^2.0.0", + "browserify": "^16.2.0", "chai": "^4.0.1", - "coveralls": "^3.0.0", - "del-cli": "^1.1.0", + "coveralls": "^3.0.1", + "del-cli": "^3.0.0", "dot": "^1.0.3", - "eslint": "^4.1.0", - "gh-pages-generator": "^0.2.0", + "eslint": "^7.3.1", + "gh-pages-generator": "^0.2.3", "glob": "^7.0.0", "if-node-version": "^1.0.0", "js-beautify": "^1.7.3", - "jshint": "^2.9.4", - "json-schema-test": "^1.3.0", - "karma": "^1.0.0", - "karma-chrome-launcher": "^2.0.0", - "karma-mocha": "^1.1.1", - "karma-phantomjs-launcher": "^1.0.0", - "karma-sauce-launcher": "^1.1.0", - "mocha": "^4.0.0", - "nodent": "^3.0.17", - "nyc": "^11.0.2", - "phantomjs-prebuilt": "^2.1.4", + "jshint": "^2.10.2", + "json-schema-test": "^2.0.0", + "karma": "^5.0.0", + "karma-chrome-launcher": "^3.0.0", + "karma-mocha": "^2.0.0", + "karma-sauce-launcher": "^4.1.3", + "mocha": "^8.0.1", + "nyc": "^15.0.0", "pre-commit": "^1.1.1", - "regenerator": "0.10.0", "require-globify": "^1.3.0", - "typescript": "^2.0.3", - "uglify-js": "^3.1.5", + "typescript": "^3.9.5", + "uglify-js": "^3.6.9", "watch": "^1.0.0" + }, + "collective": { + "type": "opencollective", + "url": "https://opencollective.com/ajv" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } } diff --git a/scripts/prepare-tests b/scripts/prepare-tests index 6f62634e2..684703318 100755 --- a/scripts/prepare-tests +++ b/scripts/prepare-tests @@ -4,6 +4,9 @@ set -e mkdir -p .browser +echo +echo Preparing browser tests: + find spec -type f -name '*.spec.js' | \ xargs -I {} sh -c \ -'export f="{}"; browserify $f -t require-globify -t brfs -x ajv -u buffer -o $(echo $f | sed -e "s/spec/.browser/");' +'export f="{}"; echo $f; browserify $f -t require-globify -t brfs -x ajv -u buffer -o $(echo $f | sed -e "s/spec/.browser/");' diff --git a/scripts/publish-built-version b/scripts/publish-built-version new file mode 100755 index 000000000..1b5712372 --- /dev/null +++ b/scripts/publish-built-version @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -e + +if [[ -n $TRAVIS_TAG && $TRAVIS_JOB_NUMBER =~ ".3" ]]; then + echo "About to publish $TRAVIS_TAG to ajv-dist..." + + git config user.email "$GIT_USER_EMAIL" + git config user.name "$GIT_USER_NAME" + + git clone https://${GITHUB_TOKEN}@github.com/ajv-validator/ajv-dist.git ../ajv-dist + + rm -rf ../ajv-dist/dist + mkdir ../ajv-dist/dist + cp ./dist/ajv.* ../ajv-dist/dist + cat bower.json | sed 's/"name": "ajv"/"name": "ajv-dist"/' > ../ajv-dist/bower.json + cd ../ajv-dist + + if [[ `git status --porcelain` ]]; then + echo "Changes detected. Updating master branch..." + git add -A + git commit -m "updated by travis build #$TRAVIS_BUILD_NUMBER" + git push --quiet origin master > /dev/null 2>&1 + fi + + echo "Publishing tag..." + + git tag $TRAVIS_TAG + git push --tags > /dev/null 2>&1 + + echo "Done" +fi diff --git a/scripts/travis-gh-pages b/scripts/travis-gh-pages index 46ded1611..b3d4f3d0f 100755 --- a/scripts/travis-gh-pages +++ b/scripts/travis-gh-pages @@ -5,7 +5,7 @@ set -e if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" && $TRAVIS_JOB_NUMBER =~ ".3" ]]; then git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qE '\.md$|^LICENSE$|travis-gh-pages$' && { rm -rf ../gh-pages - git clone -b gh-pages --single-branch https://${GITHUB_TOKEN}@github.com/epoberezkin/ajv.git ../gh-pages + git clone -b gh-pages --single-branch https://${GITHUB_TOKEN}@github.com/ajv-validator/ajv.git ../gh-pages mkdir -p ../gh-pages/_source cp *.md ../gh-pages/_source cp LICENSE ../gh-pages/_source diff --git a/spec/.eslintrc.yml b/spec/.eslintrc.yml index d2d4eda16..f9c66d538 100644 --- a/spec/.eslintrc.yml +++ b/spec/.eslintrc.yml @@ -8,3 +8,4 @@ globals: it: false before: false beforeEach: false + afterEach: false diff --git a/spec/JSON-Schema-Test-Suite b/spec/JSON-Schema-Test-Suite index 8758156cb..eadeacb04 160000 --- a/spec/JSON-Schema-Test-Suite +++ b/spec/JSON-Schema-Test-Suite @@ -1 +1 @@ -Subproject commit 8758156cb3bae615e5e75abcab6e757883d10669 +Subproject commit eadeacb04209a18fc81f1a1959e83eef72dcc97a diff --git a/spec/ajv-async.js b/spec/ajv-async.js new file mode 100644 index 000000000..d14125691 --- /dev/null +++ b/spec/ajv-async.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = typeof window == 'object' ? window.ajvAsync : require('' + 'ajv-async'); diff --git a/spec/ajv.spec.js b/spec/ajv.spec.js index d0fb3de3e..118a827ad 100644 --- a/spec/ajv.spec.js +++ b/spec/ajv.spec.js @@ -2,7 +2,7 @@ var Ajv = require('./ajv') , should = require('./chai').should() - , stableStringify = require('json-stable-stringify'); + , stableStringify = require('fast-json-stable-stringify'); describe('Ajv', function () { @@ -27,15 +27,15 @@ describe('Ajv', function () { }); it('should cache compiled functions for the same schema', function() { - var v1 = ajv.compile({ id: '//e.com/int.json', type: 'integer', minimum: 1 }); - var v2 = ajv.compile({ id: '//e.com/int.json', minimum: 1, type: 'integer' }); + var v1 = ajv.compile({ $id: '//e.com/int.json', type: 'integer', minimum: 1 }); + var v2 = ajv.compile({ $id: '//e.com/int.json', minimum: 1, type: 'integer' }); v1 .should.equal(v2); }); it('should throw if different schema has the same id', function() { - ajv.compile({ id: '//e.com/int.json', type: 'integer' }); + ajv.compile({ $id: '//e.com/int.json', type: 'integer' }); should.throw(function() { - ajv.compile({ id: '//e.com/int.json', type: 'integer', minimum: 1 }); + ajv.compile({ $id: '//e.com/int.json', type: 'integer', minimum: 1 }); }); }); @@ -74,24 +74,24 @@ describe('Ajv', function () { }); it('should validate against previously compiled schema by id (also see addSchema)', function() { - ajv.validate({ id: '//e.com/int.json', type: 'integer' }, 1) .should.equal(true); + ajv.validate({ $id: '//e.com/int.json', type: 'integer' }, 1) .should.equal(true); ajv.validate('//e.com/int.json', 1) .should.equal(true); ajv.validate('//e.com/int.json', '1') .should.equal(false); - ajv.compile({ id: '//e.com/str.json', type: 'string' }) .should.be.a('function'); + ajv.compile({ $id: '//e.com/str.json', type: 'string' }) .should.be.a('function'); ajv.validate('//e.com/str.json', 'a') .should.equal(true); ajv.validate('//e.com/str.json', 1) .should.equal(false); }); it('should throw exception if no schema with ref', function() { - ajv.validate({ id: 'integer', type: 'integer' }, 1) .should.equal(true); + ajv.validate({ $id: 'integer', type: 'integer' }, 1) .should.equal(true); ajv.validate('integer', 1) .should.equal(true); should.throw(function() { ajv.validate('string', 'foo'); }); }); it('should validate schema fragment by ref', function() { ajv.addSchema({ - "id": "http://e.com/types.json", + "$id": "http://e.com/types.json", "definitions": { "int": { "type": "integer" }, "str": { "type": "string" } @@ -104,10 +104,10 @@ describe('Ajv', function () { it('should return schema fragment by id', function() { ajv.addSchema({ - "id": "http://e.com/types.json", + "$id": "http://e.com/types.json", "definitions": { - "int": { "id": "#int", "type": "integer" }, - "str": { "id": "#str", "type": "string" } + "int": { "$id": "#int", "type": "integer" }, + "str": { "$id": "#str", "type": "string" } } }); @@ -119,8 +119,7 @@ describe('Ajv', function () { describe('addSchema method', function() { it('should add and compile schema with key', function() { - var res = ajv.addSchema({ type: 'integer' }, 'int'); - should.not.exist(res); + ajv.addSchema({ type: 'integer' }, 'int'); var validate = ajv.getSchema('int'); validate .should.be.a('function'); @@ -138,13 +137,13 @@ describe('Ajv', function () { }); it('should add and compile schema with id', function() { - ajv.addSchema({ id: '//e.com/int.json', type: 'integer' }); + ajv.addSchema({ $id: '//e.com/int.json', type: 'integer' }); ajv.validate('//e.com/int.json', 1) .should.equal(true); ajv.validate('//e.com/int.json', '1') .should.equal(false); }); it('should normalize schema keys and ids', function() { - ajv.addSchema({ id: '//e.com/int.json#', type: 'integer' }, 'int#'); + ajv.addSchema({ $id: '//e.com/int.json#', type: 'integer' }, 'int#'); ajv.validate('int', 1) .should.equal(true); ajv.validate('int', '1') .should.equal(false); ajv.validate('//e.com/int.json', 1) .should.equal(true); @@ -157,8 +156,8 @@ describe('Ajv', function () { it('should add and compile array of schemas with ids', function() { ajv.addSchema([ - { id: '//e.com/int.json', type: 'integer' }, - { id: '//e.com/str.json', type: 'string' } + { $id: '//e.com/int.json', type: 'integer' }, + { $id: '//e.com/str.json', type: 'string' } ]); var validate0 = ajv.getSchema('//e.com/int.json'); @@ -211,12 +210,17 @@ describe('Ajv', function () { it('should throw if schema id is not a string', function() { try { - ajv.addSchema({ id: 1, type: 'integer' }); + ajv.addSchema({ $id: 1, type: 'integer' }); throw new Error('should have throw exception'); } catch(e) { e.message .should.equal('schema id must be string'); } }); + + it('should return instance of itself', function() { + var res = ajv.addSchema({ type: 'integer' }, 'int'); + res.should.equal(ajv); + }); }); @@ -229,7 +233,7 @@ describe('Ajv', function () { }); it('should return compiled schema by id or ref', function() { - ajv.addSchema({ id: '//e.com/int.json', type: 'integer' }); + ajv.addSchema({ $id: '//e.com/int.json', type: 'integer' }); var validate = ajv.getSchema('//e.com/int.json'); validate(1) .should.equal(true); validate('1') .should.equal(false); @@ -248,7 +252,7 @@ describe('Ajv', function () { it('should return schema fragment by ref', function() { ajv.addSchema({ - "id": "http://e.com/types.json", + "$id": "http://e.com/types.json", "definitions": { "int": { "type": "integer" }, "str": { "type": "string" } @@ -262,7 +266,7 @@ describe('Ajv', function () { it('should return schema fragment by ref with protocol-relative URIs', function() { ajv.addSchema({ - "id": "//e.com/types.json", + "$id": "//e.com/types.json", "definitions": { "int": { "type": "integer" }, "str": { "type": "string" } @@ -276,10 +280,10 @@ describe('Ajv', function () { it('should return schema fragment by id', function() { ajv.addSchema({ - "id": "http://e.com/types.json", + "$id": "http://e.com/types.json", "definitions": { - "int": { "id": "#int", "type": "integer" }, - "str": { "id": "#str", "type": "string" } + "int": { "$id": "#int", "type": "integer" }, + "str": { "$id": "#str", "type": "string" } } }); @@ -306,7 +310,7 @@ describe('Ajv', function () { }); it('should remove schema by id', function() { - var schema = { id: '//e.com/int.json', type: 'integer' } + var schema = { $id: '//e.com/int.json', type: 'integer' } , str = stableStringify(schema); ajv.addSchema(schema); @@ -329,11 +333,11 @@ describe('Ajv', function () { }); it('should remove schema with id by schema object', function() { - var schema = { id: '//e.com/int.json', type: 'integer' } + var schema = { $id: '//e.com/int.json', type: 'integer' } , str = stableStringify(schema); ajv.addSchema(schema); ajv._cache.get(str) .should.be.an('object'); - ajv.removeSchema({ id: '//e.com/int.json', type: 'integer' }); + ajv.removeSchema({ $id: '//e.com/int.json', type: 'integer' }); // should.not.exist(ajv.getSchema('//e.com/int.json')); should.not.exist(ajv._cache.get(str)); }); @@ -346,7 +350,7 @@ describe('Ajv', function () { }); it('should remove all schemas but meta-schemas if called without an arguments', function() { - var schema1 = { id: '//e.com/int.json', type: 'integer' } + var schema1 = { $id: '//e.com/int.json', type: 'integer' } , str1 = stableStringify(schema1); ajv.addSchema(schema1); ajv._cache.get(str1) .should.be.an('object'); @@ -362,12 +366,12 @@ describe('Ajv', function () { }); it('should remove all schemas but meta-schemas with key/id matching pattern', function() { - var schema1 = { id: '//e.com/int.json', type: 'integer' } + var schema1 = { $id: '//e.com/int.json', type: 'integer' } , str1 = stableStringify(schema1); ajv.addSchema(schema1); ajv._cache.get(str1) .should.be.an('object'); - var schema2 = { id: 'str.json', type: 'string' } + var schema2 = { $id: 'str.json', type: 'string' } , str2 = stableStringify(schema2); ajv.addSchema(schema2, '//e.com/str.json'); ajv._cache.get(str2) .should.be.an('object'); @@ -382,6 +386,13 @@ describe('Ajv', function () { should.not.exist(ajv._cache.get(str2)); ajv._cache.get(str3) .should.be.an('object'); }); + + it('should return instance of itself', function() { + var res = ajv + .addSchema({ type: 'integer' }, 'int') + .removeSchema('int'); + res.should.equal(ajv); + }); }); @@ -408,6 +419,11 @@ describe('Ajv', function () { testFormat(); }); + it('should return instance of itself', function() { + var res = ajv.addFormat('identifier', /^[a-z_$][a-z0-9_$]*$/i); + res.should.equal(ajv); + }); + function testFormat() { var validate = ajv.compile({ format: 'identifier' }); validate('Abc1') .should.equal(true); @@ -460,7 +476,7 @@ describe('Ajv', function () { describe('validateSchema method', function() { it('should validate schema against meta-schema', function() { var valid = ajv.validateSchema({ - $schema: 'http://json-schema.org/draft-06/schema#', + $schema: 'http://json-schema.org/draft-07/schema#', type: 'number' }); @@ -468,7 +484,7 @@ describe('Ajv', function () { should.equal(ajv.errors, null); valid = ajv.validateSchema({ - $schema: 'http://json-schema.org/draft-06/schema#', + $schema: 'http://json-schema.org/draft-07/schema#', type: 'wrong_type' }); @@ -496,5 +512,56 @@ describe('Ajv', function () { }); }); }); + + describe('sub-schema validation outside of definitions during compilation', function() { + it('maximum', function() { + passValidationThrowCompile({ + $ref: '#/foo', + foo: {maximum: 'bar'} + }); + }); + + it('exclusiveMaximum', function() { + passValidationThrowCompile({ + $ref: '#/foo', + foo: {exclusiveMaximum: 'bar'} + }); + }); + + it('maxItems', function() { + passValidationThrowCompile({ + $ref: '#/foo', + foo: {maxItems: 'bar'} + }); + }); + + it('maxLength', function() { + passValidationThrowCompile({ + $ref: '#/foo', + foo: {maxLength: 'bar'} + }); + }); + + it('maxProperties', function() { + passValidationThrowCompile({ + $ref: '#/foo', + foo: {maxProperties: 'bar'} + }); + }); + + it('multipleOf', function() { + passValidationThrowCompile({ + $ref: '#/foo', + foo: {maxProperties: 'bar'} + }); + }); + + function passValidationThrowCompile(schema) { + ajv.validateSchema(schema) .should.equal(true); + should.throw(function() { + ajv.compile(schema); + }); + } + }); }); }); diff --git a/spec/ajv_async_instances.js b/spec/ajv_async_instances.js index 85854f37c..8facd3637 100644 --- a/spec/ajv_async_instances.js +++ b/spec/ajv_async_instances.js @@ -2,95 +2,31 @@ var Ajv = require('./ajv') , util = require('../lib/compile/util') - , setupAsync = require('ajv-async'); + , setupAsync = require('./ajv-async'); module.exports = getAjvInstances; - var firstTime = true; -var isBrowser = typeof window == 'object'; -var fullTest = isBrowser || !process.env.AJV_FAST_TEST; - function getAjvInstances(opts) { opts = opts || {}; var instances = []; var options = [ {}, - { async: true }, - { async: 'co*' }, - { async: 'es7' }, - { async: 'es7', transpile: 'nodent' }, - { async: 'co*', allErrors: true }, - { async: 'es7', allErrors: true }, - { async: 'es7', transpile: 'nodent', allErrors: true } + { transpile: true }, + { allErrors: true }, + { transpile: true, allErrors: true } ]; - var ua; - try { ua = window.navigator.userAgent.toLowerCase(); } catch(e) {} - - // regenerator does not work in IE9 - if (!(ua && /msie\s9/.test(ua))) { - options = options.concat([ - { async: '*', transpile: 'regenerator' }, - { async: '*', transpile: 'regenerator', allErrors: true } - ]); - } - - if (fullTest) { - options = options.concat([ - { async: '*' }, - { allErrors: true }, - { async: true, allErrors: true }, - { async: '*', allErrors: true } - ]); - - if (!(ua && /msie\s9/.test(ua))) { - options = options.concat([ - { async: 'co*', transpile: 'regenerator' }, - { async: 'co*', transpile: 'regenerator', allErrors: true } - ]); - } - - // es7 functions transpiled with regenerator are excluded from test in Safari/Firefox/Edge/IE9. - // They fail in IE9 and emit multiple 'uncaught exception' warnings in Safari/Firefox/Edge anc cause remote tests to disconnect. - if (!(ua && ((/safari/.test(ua) && !/chrome|phantomjs/.test(ua)) || /firefox|edge|msie\s9/.test(ua)))) { - options = options.concat([ - { transpile: 'regenerator' }, - { async: true, transpile: 'regenerator' }, - { async: 'es7', transpile: 'regenerator' }, - { transpile: 'regenerator', allErrors: true }, - { async: true, transpile: 'regenerator', allErrors: true }, - { async: 'es7', transpile: 'regenerator', allErrors: true } - ]); - } - } - - // options = options.filter(function (_opts) { - // return _opts.transpile == 'nodent'; - // }); - - // var i = 10, repeatOptions = []; - // while (i--) repeatOptions = repeatOptions.concat(options); - // options = repeatOptions; - options.forEach(function (_opts) { util.copy(opts, _opts); var ajv = getAjv(_opts); if (ajv) instances.push(ajv); }); - if (firstTime) { - var asyncModes = []; - instances.forEach(function (ajv) { - if (!ajv._opts.async) return; - var t = ajv._opts.transpile; - var mode = ajv._opts.async + (t === true ? '' : '.' + t); - if (asyncModes.indexOf(mode) == -1) asyncModes.push(mode); - }); - console.log('Testing', instances.length, 'ajv instances:', asyncModes.join(',')); + console.log('Testing', instances.length, 'ajv instances:'); firstTime = false; } diff --git a/spec/async.spec.js b/spec/async.spec.js index 5363e32dd..5b4c5ff6e 100644 --- a/spec/async.spec.js +++ b/spec/async.spec.js @@ -10,56 +10,74 @@ describe('compileAsync method', function() { var SCHEMAS = { "http://example.com/object.json": { - "id": "http://example.com/object.json", + "$id": "http://example.com/object.json", "properties": { "a": { "type": "string" }, "b": { "$ref": "int2plus.json" } } }, "http://example.com/int2plus.json": { - "id": "http://example.com/int2plus.json", + "$id": "http://example.com/int2plus.json", "type": "integer", "minimum": 2 }, "http://example.com/tree.json": { - "id": "http://example.com/tree.json", + "$id": "http://example.com/tree.json", "type": "array", "items": { "$ref": "leaf.json" } }, "http://example.com/leaf.json": { - "id": "http://example.com/leaf.json", + "$id": "http://example.com/leaf.json", "properties": { "name": { "type": "string" }, "subtree": { "$ref": "tree.json" } } }, "http://example.com/recursive.json": { - "id": "http://example.com/recursive.json", + "$id": "http://example.com/recursive.json", "properties": { "b": { "$ref": "parent.json" } }, "required": ["b"] }, "http://example.com/invalid.json": { - "id": "http://example.com/recursive.json", + "$id": "http://example.com/recursive.json", "properties": { "invalid": { "type": "number" } }, "required": "invalid" }, "http://example.com/foobar.json": { - "id": "http://example.com/foobar.json", + "$id": "http://example.com/foobar.json", "$schema": "http://example.com/foobar_meta.json", "myFooBar": "foo" }, "http://example.com/foobar_meta.json": { - "id": "http://example.com/foobar_meta.json", + "$id": "http://example.com/foobar_meta.json", "type": "object", "properties": { "myFooBar": { "enum": ["foo", "bar"] } } + }, + "http://example.com/foo.json": { + "$id": "http://example.com/foo.json", + "type": "object", + "properties": { + "bar": {"$ref": "bar.json"}, + "other": {"$ref": "other.json"} + } + }, + "http://example.com/bar.json": { + "$id": "http://example.com/bar.json", + "type": "object", + "properties": { + "foo": {"$ref": "foo.json"} + } + }, + "http://example.com/other.json": { + "$id": "http://example.com/other.json" } }; @@ -71,7 +89,7 @@ describe('compileAsync method', function() { it('should compile schemas loading missing schemas with options.loadSchema function', function() { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "object.json" } } @@ -87,7 +105,7 @@ describe('compileAsync method', function() { it('should compile schemas loading missing schemas and return function via callback', function (done) { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "object.json" } } @@ -105,7 +123,7 @@ describe('compileAsync method', function() { it('should correctly load schemas when missing reference has JSON path', function() { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "object.json#/properties/b" } } @@ -121,7 +139,7 @@ describe('compileAsync method', function() { it('should correctly compile with remote schemas that have mutual references', function() { var schema = { - "id": "http://example.com/root.json", + "$id": "http://example.com/root.json", "properties": { "tree": { "$ref": "tree.json" } } @@ -143,7 +161,7 @@ describe('compileAsync method', function() { it('should correctly compile with remote schemas that reference the compiled schema', function() { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "recursive.json" } } @@ -161,7 +179,7 @@ describe('compileAsync method', function() { it('should resolve reference containing "properties" segment with the same property (issue #220)', function() { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "object.json#/properties/a" @@ -206,7 +224,7 @@ describe('compileAsync method', function() { it('should return compiled schema on the next tick if there are no references (#51)', function() { var schema = { - "id": "http://example.com/int2plus.json", + "$id": "http://example.com/int2plus.json", "type": "integer", "minimum": 2 }; @@ -238,7 +256,7 @@ describe('compileAsync method', function() { it('should queue calls so only one compileAsync executes at a time (#52)', function() { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "object.json" } } @@ -261,7 +279,7 @@ describe('compileAsync method', function() { it('should throw exception if loadSchema is not passed', function (done) { var schema = { - "id": "http://example.com/int2plus.json", + "$id": "http://example.com/int2plus.json", "type": "integer", "minimum": 2 }; @@ -281,7 +299,7 @@ describe('compileAsync method', function() { describe('should return error via callback', function() { it('if passed schema is invalid', function (done) { var invalidSchema = { - "id": "http://example.com/int2plus.json", + "$id": "http://example.com/int2plus.json", "type": "integer", "minimum": "invalid" }; @@ -290,7 +308,7 @@ describe('compileAsync method', function() { it('if loaded schema is invalid', function (done) { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "invalid.json" } } @@ -300,7 +318,7 @@ describe('compileAsync method', function() { it('if required schema is loaded but the reference cannot be resolved', function (done) { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "object.json#/definitions/not_found" } } @@ -310,7 +328,7 @@ describe('compileAsync method', function() { it('if loadSchema returned error', function (done) { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "object.json" } } @@ -346,7 +364,7 @@ describe('compileAsync method', function() { describe('should return error via promise', function() { it('if passed schema is invalid', function() { var invalidSchema = { - "id": "http://example.com/int2plus.json", + "$id": "http://example.com/int2plus.json", "type": "integer", "minimum": "invalid" }; @@ -355,7 +373,7 @@ describe('compileAsync method', function() { it('if loaded schema is invalid', function() { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "invalid.json" } } @@ -365,7 +383,7 @@ describe('compileAsync method', function() { it('if required schema is loaded but the reference cannot be resolved', function() { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "object.json#/definitions/not_found" } } @@ -375,7 +393,7 @@ describe('compileAsync method', function() { it('if loadSchema returned error', function() { var schema = { - "id": "http://example.com/parent.json", + "$id": "http://example.com/parent.json", "properties": { "a": { "$ref": "object.json" } } @@ -412,6 +430,23 @@ describe('compileAsync method', function() { }); + describe('schema with multiple remote properties, the first is recursive schema (#801)', function() { + it('should validate data', function() { + var schema = { + "$id": "http://example.com/list.json", + "type": "object", + "properties": { + "foo": {"$ref": "foo.json"} + } + }; + + return ajv.compileAsync(schema).then(function (validate) { + validate({foo: {}}) .should.equal(true); + }); + }); + }); + + function loadSchema(uri) { loadCallCount++; return new Promise(function (resolve, reject) { diff --git a/spec/async_validate.spec.js b/spec/async_validate.spec.js index fdfdf5dde..04d47cece 100644 --- a/spec/async_validate.spec.js +++ b/spec/async_validate.spec.js @@ -3,9 +3,7 @@ var Ajv = require('./ajv') , Promise = require('./promise') , getAjvInstances = require('./ajv_async_instances') - , should = require('./chai').should() - , co = require('co') - , setupAsync = require('ajv-async'); + , should = require('./chai').should(); describe('async schemas, formats and keywords', function() { @@ -17,12 +15,6 @@ describe('async schemas, formats and keywords', function() { ajv = instances[0]; }); - function useCo(_ajv) { - var async = _ajv._opts.async; - return async == 'es7' || async == 'co*' ? identity : co; - } - - function identity(x) { return x; } describe('async schemas without async elements', function() { it('should return result as promise', function() { @@ -36,12 +28,11 @@ describe('async schemas, formats and keywords', function() { function test(_ajv) { var validate = _ajv.compile(schema); - var _co = useCo(_ajv); return Promise.all([ - shouldBeValid( _co(validate('abc')), 'abc' ), - shouldBeInvalid( _co(validate('abcd')) ), - shouldBeInvalid( _co(validate(1)) ), + shouldBeValid( validate('abc'), 'abc' ), + shouldBeInvalid( validate('abcd') ), + shouldBeInvalid( validate(1) ), ]); } }); @@ -149,11 +140,10 @@ describe('async schemas, formats and keywords', function() { }; var validate = _ajv.compile(schema); - var _co = useCo(_ajv); return Promise.all([ - shouldBeInvalid(_co(validate({ userId: 5, postId: 10 })), [ 'id not found in table posts' ]), - shouldBeInvalid(_co(validate({ userId: 9, postId: 25 })), [ 'id not found in table users' ]) + shouldBeInvalid( validate({ userId: 5, postId: 10 }), [ 'id not found in table posts' ] ), + shouldBeInvalid( validate({ userId: 9, postId: 25 }), [ 'id not found in table users' ] ) ]); })); }); @@ -214,14 +204,13 @@ describe('async schemas, formats and keywords', function() { return repeat(function() { return Promise.all(instances.map(function (_ajv) { var validate = _ajv.compile(schema); - var _co = useCo(_ajv); var validData = { word: 'tomorrow' }; return Promise.all([ - shouldBeValid( _co(validate(validData)), validData ), - shouldBeInvalid( _co(validate({ word: 'manana' })) ), - shouldBeInvalid( _co(validate({ word: 1 })) ), - shouldThrow( _co(validate({ word: 'today' })), 'unknown word' ) + shouldBeValid( validate(validData), validData ), + shouldBeInvalid( validate({ word: 'manana' }) ), + shouldBeInvalid( validate({ word: 1 }) ), + shouldThrow( validate({ word: 'today' }), 'unknown word' ) ]); })); }); }); @@ -250,6 +239,32 @@ describe('async schemas, formats and keywords', function() { return recursiveTest(schema); }); + it('should validate recursive ref to async sub-schema, issue #612', function() { + var schema = { + $async: true, + type: 'object', + properties: { + foo: { + $async: true, + anyOf: [ + { + type: 'string', + format: 'english_word' + }, + { + type: 'object', + properties: { + foo: { $ref: '#/properties/foo' } + } + } + ] + } + } + }; + + return recursiveTest(schema); + }); + it('should validate ref from referenced async schema to root schema', function() { var schema = { $async: true, @@ -276,7 +291,7 @@ describe('async schemas, formats and keywords', function() { it('should validate refs between two async schemas', function() { var schemaObj = { - id: 'http://e.com/obj.json#', + $id: 'http://e.com/obj.json#', $async: true, type: 'object', properties: { @@ -285,7 +300,7 @@ describe('async schemas, formats and keywords', function() { }; var schemaWord = { - id: 'http://e.com/word.json#', + $id: 'http://e.com/word.json#', $async: true, anyOf: [ { @@ -301,7 +316,7 @@ describe('async schemas, formats and keywords', function() { it('should fail compilation if sync schema references async schema', function() { var schema = { - id: 'http://e.com/obj.json#', + $id: 'http://e.com/obj.json#', type: 'object', properties: { foo: { $ref: 'http://e.com/word.json#' } @@ -309,7 +324,7 @@ describe('async schemas, formats and keywords', function() { }; var schemaWord = { - id: 'http://e.com/word.json#', + $id: 'http://e.com/word.json#', $async: true, anyOf: [ { @@ -330,7 +345,7 @@ describe('async schemas, formats and keywords', function() { ajv.compile(schema); }); - schema.id = 'http://e.com/obj2.json#'; + schema.$id = 'http://e.com/obj2.json#'; schema.$async = true; ajv.compile(schema); @@ -340,22 +355,21 @@ describe('async schemas, formats and keywords', function() { return repeat(function() { return Promise.all(instances.map(function (_ajv) { if (refSchema) try { _ajv.addSchema(refSchema); } catch(e) {} var validate = _ajv.compile(schema); - var _co = useCo(_ajv); var data; return Promise.all([ - shouldBeValid( _co(validate(data = { foo: 'tomorrow' })), data ), - shouldBeInvalid( _co(validate({ foo: 'manana' })) ), - shouldBeInvalid( _co(validate({ foo: 1 })) ), - shouldThrow( _co(validate({ foo: 'today' })), 'unknown word' ), - shouldBeValid( _co(validate(data = { foo: { foo: 'tomorrow' }})), data ), - shouldBeInvalid( _co(validate({ foo: { foo: 'manana' }})) ), - shouldBeInvalid( _co(validate({ foo: { foo: 1 }})) ), - shouldThrow( _co(validate({ foo: { foo: 'today' }})), 'unknown word' ), - shouldBeValid( _co(validate(data = { foo: { foo: { foo: 'tomorrow' }}})), data ), - shouldBeInvalid( _co(validate({ foo: { foo: { foo: 'manana' }}})) ), - shouldBeInvalid( _co(validate({ foo: { foo: { foo: 1 }}})) ), - shouldThrow( _co(validate({ foo: { foo: { foo: 'today' }}})), 'unknown word' ) + shouldBeValid( validate(data = { foo: 'tomorrow' }), data ), + shouldBeInvalid( validate({ foo: 'manana' }) ), + shouldBeInvalid( validate({ foo: 1 }) ), + shouldThrow( validate({ foo: 'today' }), 'unknown word' ), + shouldBeValid( validate(data = { foo: { foo: 'tomorrow' }}), data ), + shouldBeInvalid( validate({ foo: { foo: 'manana' }}) ), + shouldBeInvalid( validate({ foo: { foo: 1 }}) ), + shouldThrow( validate({ foo: { foo: 'today' }}), 'unknown word' ), + shouldBeValid( validate(data = { foo: { foo: { foo: 'tomorrow' }}}), data ), + shouldBeInvalid( validate({ foo: { foo: { foo: 'manana' }}}) ), + shouldBeInvalid( validate({ foo: { foo: { foo: 1 }}}) ), + shouldThrow( validate({ foo: { foo: { foo: 'today' }}}), 'unknown word' ) ]); })); }); } @@ -373,35 +387,6 @@ describe('async schemas, formats and keywords', function() { }); -describe('async/transpile option', function() { - it('should throw error with unknown async option', function() { - shouldThrowFunc('bad async mode: es8', function() { - setupAsync(new Ajv({ async: 'es8' })); - }); - }); - - - it('should throw error with unknown transpile option', function() { - shouldThrowFunc('bad transpiler: babel', function() { - setupAsync(new Ajv({ transpile: 'babel' })); - }); - - shouldThrowFunc('bad transpiler: [object Object]', function() { - setupAsync(new Ajv({ transpile: {} })); - }); - }); - - - it('should set async option to es7 if tranpiler is nodent', function() { - var ajv1 = setupAsync(new Ajv({ transpile: 'nodent' })); - ajv1._opts.async .should.equal('es7'); - - var ajv2 = setupAsync(new Ajv({ async: '*', transpile: 'nodent' })); - ajv2._opts.async .should.equal('es7'); - }); -}); - - function checkWordOnServer(str) { return str == 'tomorrow' ? Promise.resolve(true) : str == 'manana' ? Promise.resolve(false) diff --git a/spec/browser_test_suite.js b/spec/browser_test_suite.js index 91334330a..40e138c7c 100644 --- a/spec/browser_test_suite.js +++ b/spec/browser_test_suite.js @@ -2,6 +2,8 @@ module.exports = function (suite) { suite.forEach(function (file) { + if (file.name.indexOf('optional/format') == 0) + file.name = file.name.replace('optional/', ''); file.test = file.module; }); return suite; diff --git a/spec/coercion.spec.js b/spec/coercion.spec.js index cc197c2ce..a9f13de5c 100644 --- a/spec/coercion.spec.js +++ b/spec/coercion.spec.js @@ -260,7 +260,7 @@ describe('Type coercion', function () { }); - it('should coerce to multiple types in order', function() { + it('should coerce to multiple types in order with number type', function() { var schema = { type: 'object', properties: { @@ -302,6 +302,45 @@ describe('Type coercion', function () { }); }); + it('should coerce to multiple types in order with integer type', function() { + var schema = { + type: 'object', + properties: { + foo: { + type: [ 'integer', 'boolean', 'null' ] + } + } + }; + + instances.forEach(function (_ajv) { + var data; + + _ajv.validate(schema, data = { foo: '1' }) .should.equal(true); + data .should.eql({ foo: 1 }); + + _ajv.validate(schema, data = { foo: 'false' }) .should.equal(true); + data .should.eql({ foo: false }); + + _ajv.validate(schema, data = { foo: 1 }) .should.equal(true); + data .should.eql({ foo: 1 }); // no coercion + + _ajv.validate(schema, data = { foo: true }) .should.equal(true); + data .should.eql({ foo: true }); // no coercion + + _ajv.validate(schema, data = { foo: null }) .should.equal(true); + data .should.eql({ foo: null }); // no coercion + + _ajv.validate(schema, data = { foo: 'abc' }) .should.equal(false); + data .should.eql({ foo: 'abc' }); // can't coerce + + _ajv.validate(schema, data = { foo: {} }) .should.equal(false); + data .should.eql({ foo: {} }); // can't coerce + + _ajv.validate(schema, data = { foo: [] }) .should.equal(false); + data .should.eql({ foo: [] }); // can't coerce + }); + }); + it('should fail to coerce non-number if multiple properties/items are coerced (issue #152)', function() { var schema = { @@ -365,10 +404,10 @@ describe('Type coercion', function () { }; var schemaRecursive2 = { - id: 'http://e.com/schema.json#', + $id: 'http://e.com/schema.json#', definitions: { foo: { - id: 'http://e.com/foo.json#', + $id: 'http://e.com/foo.json#', type: [ 'object', 'number' ], properties: { foo: { $ref: '#' } @@ -416,6 +455,39 @@ describe('Type coercion', function () { }); + it('should check "uniqueItems" after coercion', function() { + var schema = { + items: {type: 'number'}, + uniqueItems: true + }; + + instances.forEach(function (_ajv) { + var validate = _ajv.compile(schema); + validate([1, '2', 3]). should.equal(true); + + validate([1, '2', 2]). should.equal(false); + validate.errors.length .should.equal(1); + validate.errors[0].keyword .should.equal('uniqueItems'); + }); + }); + + + it('should check "contains" after coercion', function() { + var schema = { + items: {type: 'number'}, + contains: {const: 2} + }; + + instances.forEach(function (_ajv) { + var validate = _ajv.compile(schema); + validate([1, '2', 3]). should.equal(true); + + validate([1, '3', 4]). should.equal(false); + validate.errors.pop().keyword .should.equal('contains'); + }); + }); + + function testRules(rules, cb) { for (var toType in rules) { for (var fromType in rules[toType]) { diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 4ed6e1355..2924fceea 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -209,6 +209,44 @@ describe('Custom keywords', function () { testMultipleRangeKeyword({ type: 'number', macro: macroRange }, 2); }); + it('should support resolving $ref without id or $id', function () { + instances.forEach(function (_ajv) { + _ajv.addKeyword('macroRef', { + macro: function (schema, parentSchema, it) { + it.baseId .should.equal('#'); + var ref = schema.$ref; + var validate = _ajv.getSchema(ref); + if (validate) return validate.schema; + throw new ajv.constructor.MissingRefError(it.baseId, ref); + }, + metaSchema: { + "type": "object", + "required": [ "$ref" ], + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string" + } + } + } + }); + var schema = { + "macroRef": { + "$ref": "#/definitions/schema" + }, + "definitions": { + "schema": { + "type": "string" + } + } + }; + var validate; + (function compileMacroRef () { validate = _ajv.compile(schema); }).should.not.throw(); + shouldBeValid(validate, 'foo'); + shouldBeInvalid(validate, 1, 2); + }); + }); + it('should recursively expand macro keywords', function() { instances.forEach(function (_ajv) { _ajv.addKeyword('deepProperties', { type: 'object', macro: macroDeepProperties }); @@ -377,6 +415,7 @@ describe('Custom keywords', function () { it('should correctly expand macros in macro expansions', function() { instances.forEach(function (_ajv) { _ajv.addKeyword('range', { type: 'number', macro: macroRange }); + _ajv.addKeyword('exclusiveRange', { metaSchema: {type: 'boolean'} }); _ajv.addKeyword('myContains', { type: 'array', macro: macroContains }); var schema = { @@ -773,6 +812,7 @@ describe('Custom keywords', function () { function testRangeKeyword(definition, customErrors, numErrors) { instances.forEach(function (_ajv) { _ajv.addKeyword('x-range', definition); + _ajv.addKeyword('exclusiveRange', {metaSchema: {type: 'boolean'}}); var schema = { "x-range": [2, 4] }; var validate = _ajv.compile(schema); @@ -811,6 +851,7 @@ describe('Custom keywords', function () { function testMultipleRangeKeyword(definition, numErrors) { instances.forEach(function (_ajv) { _ajv.addKeyword('x-range', definition); + _ajv.addKeyword('exclusiveRange', {metaSchema: {type: 'boolean'}}); var schema = { "properties": { @@ -939,6 +980,13 @@ describe('Custom keywords', function () { }); }); + it('should return instance of itself', function() { + var res = ajv.addKeyword('any', { + validate: function() { return true; } + }); + res.should.equal(ajv); + }); + it('should throw if unknown type is passed', function() { should.throw(function() { addKeyword('custom1', 'wrongtype'); @@ -1041,6 +1089,15 @@ describe('Custom keywords', function () { validate(1) .should.equal(false); validate(2) .should.equal(true); }); + + it('should return instance of itself', function() { + var res = ajv + .addKeyword('any', { + validate: function() { return true; } + }) + .removeKeyword('any'); + res.should.equal(ajv); + }); }); @@ -1128,4 +1185,50 @@ describe('Custom keywords', function () { }); }); }); + + + describe('"dependencies" in keyword definition', function() { + it("should require properties in the parent schema", function() { + ajv.addKeyword('allRequired', { + macro: function(schema, parentSchema) { + return schema ? {required: Object.keys(parentSchema.properties)} : true; + }, + metaSchema: {type: 'boolean'}, + dependencies: ['properties'] + }); + + var invalidSchema = { + allRequired: true + }; + + should.throw(function () { + ajv.compile(invalidSchema); + }); + + var schema = { + properties: { + foo: true + }, + allRequired: true + }; + + var v = ajv.compile(schema); + v({foo: 1}) .should.equal(true); + v({}) .should.equal(false); + }); + + it("'dependencies'should be array of valid strings", function() { + ajv.addKeyword('newKeyword1', { + metaSchema: {type: 'boolean'}, + dependencies: ['dep1'] + }); + + should.throw(function () { + ajv.addKeyword('newKeyword2', { + metaSchema: {type: 'boolean'}, + dependencies: [1] + }); + }); + }); + }); }); diff --git a/spec/errors.spec.js b/spec/errors.spec.js index c852b5566..6291b15ae 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -64,22 +64,23 @@ describe('Validation errors', function () { , invalidData = { foo: 1, bar: 2, baz: 3, quux: 4 }; var path = pathFunc(errorDataPath); + var msg = additionalFunc(errorDataPath); var validate = ajv.compile(schema); shouldBeValid(validate, data); shouldBeInvalid(validate, invalidData); - shouldBeError(validate.errors[0], 'additionalProperties', '#/additionalProperties', path("['baz']"), undefined, { additionalProperty: 'baz' }); + shouldBeError(validate.errors[0], 'additionalProperties', '#/additionalProperties', path("['baz']"), msg, { additionalProperty: 'baz' }); var validateJP = ajvJP.compile(schema); shouldBeValid(validateJP, data); shouldBeInvalid(validateJP, invalidData); - shouldBeError(validateJP.errors[0], 'additionalProperties', '#/additionalProperties', path("/baz"), undefined, { additionalProperty: 'baz' }); + shouldBeError(validateJP.errors[0], 'additionalProperties', '#/additionalProperties', path("/baz"), msg, { additionalProperty: 'baz' }); var fullValidate = fullAjv.compile(schema); shouldBeValid(fullValidate, data); shouldBeInvalid(fullValidate, invalidData, 2); - shouldBeError(fullValidate.errors[0], 'additionalProperties', '#/additionalProperties', path('/baz'), undefined, { additionalProperty: 'baz' }); - shouldBeError(fullValidate.errors[1], 'additionalProperties', '#/additionalProperties', path('/quux'), undefined, { additionalProperty: 'quux' }); + shouldBeError(fullValidate.errors[0], 'additionalProperties', '#/additionalProperties', path('/baz'), msg, { additionalProperty: 'baz' }); + shouldBeError(fullValidate.errors[1], 'additionalProperties', '#/additionalProperties', path('/quux'), msg, { additionalProperty: 'quux' }); if (errorDataPath == 'property') { fullValidate.errors @@ -184,7 +185,7 @@ describe('Validation errors', function () { delete invalidData2[98]; var path = pathFunc(errorDataPath); - var msg = msgFunc(errorDataPath); + var msg = requiredFunc(errorDataPath); test(); @@ -368,7 +369,7 @@ describe('Validation errors', function () { , invalidData2 = { bar: 2 }; var path = pathFunc(errorDataPath); - var msg = msgFunc(errorDataPath); + var msg = requiredFunc(errorDataPath); var validate = ajv.compile(schema); shouldBeValid(validate, data); @@ -399,7 +400,7 @@ describe('Validation errors', function () { }; } - function msgFunc(errorDataPath) { + function requiredFunc(errorDataPath) { return function (prop) { return errorDataPath == 'property' ? 'is a required property' @@ -407,10 +408,16 @@ describe('Validation errors', function () { }; } + function additionalFunc(errorDataPath) { + return errorDataPath == 'property' + ? 'is an invalid additional property' + : 'should NOT have additional properties'; + } + it('"items" errors should include item index without quotes in dataPath (#48)', function() { var schema1 = { - id: 'schema1', + $id: 'schema1', type: 'array', items: { type: 'integer', @@ -445,7 +452,7 @@ describe('Validation errors', function () { shouldBeError(fullValidate.errors[1], 'minimum', '#/items/minimum', '/3', 'should be >= 10'); var schema2 = { - id: 'schema2', + $id: 'schema2', type: 'array', items: [{ minimum: 10 }, { minimum: 9 }, { minimum: 12 }] }; @@ -539,6 +546,39 @@ describe('Validation errors', function () { validate(1.5) .should.equal(true); } }); + + it('should return passing schemas in error params', function() { + var schema = { + oneOf: [ + { type: 'number' }, + { type: 'integer' }, + { const: 1.5 } + ] + }; + + test(ajv); + test(fullAjv); + + function test(_ajv) { + var validate = _ajv.compile(schema); + validate(1) .should.equal(false); + var err = validate.errors.pop(); + err.keyword .should.equal('oneOf'); + err.params .should.eql({passingSchemas: [0, 1]}); + + validate(1.5) .should.equal(false); + err = validate.errors.pop(); + err.keyword .should.equal('oneOf'); + err.params .should.eql({passingSchemas: [0, 2]}); + + validate(2.5) .should.equal(true); + + validate('foo') .should.equal(false); + err = validate.errors.pop(); + err.keyword .should.equal('oneOf'); + err.params .should.eql({passingSchemas: null}); + } + }); }); @@ -671,6 +711,148 @@ describe('Validation errors', function () { } }); }); + + it('should include limits in error message with $data', function() { + var schema = { + "properties": { + "smaller": { + "type": "number", + "exclusiveMaximum": { "$data": "1/larger" } + }, + "larger": { "type": "number" } + } + }; + + ajv = new Ajv({$data: true}); + fullAjv = new Ajv({$data: true, allErrors: true, verbose: true, jsonPointers: true}); + + [ajv, fullAjv].forEach(function (_ajv) { + var validate = _ajv.compile(schema); + shouldBeValid(validate, {smaller: 2, larger: 4}); + shouldBeValid(validate, {smaller: 3, larger: 4}); + + shouldBeInvalid(validate, {smaller: 4, larger: 4}); + testError(); + + shouldBeInvalid(validate, {smaller: 5, larger: 4}); + testError(); + + function testError() { + var err = validate.errors[0]; + shouldBeError(err, 'exclusiveMaximum', + '#/properties/smaller/exclusiveMaximum', + _ajv._opts.jsonPointers ? '/smaller' : '.smaller', + 'should be < 4', + {comparison: '<', limit: 4, exclusive: true}); + } + }); + }); + }); + + + describe('if/then/else errors', function() { + var validate, numErrors; + + it('if/then/else should include failing keyword in message and params', function() { + var schema = { + 'if': { maximum: 10 }, + 'then': { multipleOf: 2 }, + 'else': { multipleOf: 5 } + }; + + [ajv, fullAjv].forEach(function (_ajv) { + prepareTest(_ajv, schema); + shouldBeValid(validate, 8); + shouldBeValid(validate, 15); + + shouldBeInvalid(validate, 7, numErrors); + testIfError('then', 2); + + shouldBeInvalid(validate, 17, numErrors); + testIfError('else', 5); + }); + }); + + it('if/then should include failing keyword in message and params', function() { + var schema = { + 'if': { maximum: 10 }, + 'then': { multipleOf: 2 } + }; + + [ajv, fullAjv].forEach(function (_ajv) { + prepareTest(_ajv, schema); + shouldBeValid(validate, 8); + shouldBeValid(validate, 11); + shouldBeValid(validate, 12); + + shouldBeInvalid(validate, 7, numErrors); + testIfError('then', 2); + }); + }); + + it('if/else should include failing keyword in message and params', function() { + var schema = { + 'if': { maximum: 10 }, + 'else': { multipleOf: 5 } + }; + + [ajv, fullAjv].forEach(function (_ajv) { + prepareTest(_ajv, schema); + shouldBeValid(validate, 7); + shouldBeValid(validate, 8); + shouldBeValid(validate, 15); + + shouldBeInvalid(validate, 17, numErrors); + testIfError('else', 5); + }); + }); + + function prepareTest(_ajv, schema) { + validate = _ajv.compile(schema); + numErrors = _ajv._opts.allErrors ? 2 : 1; + } + + function testIfError(ifClause, multipleOf) { + var err = validate.errors[0]; + shouldBeError(err, 'multipleOf', '#/' + ifClause + '/multipleOf', '', + 'should be multiple of ' + multipleOf, {multipleOf: multipleOf}); + + if (numErrors == 2) { + err = validate.errors[1]; + shouldBeError(err, 'if', '#/if', '', + 'should match "' + ifClause + '" schema', {failingKeyword: ifClause}); + } + } + }); + + + describe('uniqueItems errors', function() { + it('should not return uniqueItems error when non-unique items are of a different type than required', function() { + var schema = { + items: {type: 'number'}, + uniqueItems: true + }; + + [ajvJP, fullAjv].forEach(function (_ajv) { + var validate = _ajv.compile(schema); + shouldBeValid(validate, [1, 2, 3]); + + shouldBeInvalid(validate, [1, 2, 2]); + shouldBeError(validate.errors[0], 'uniqueItems', '#/uniqueItems', '', + 'should NOT have duplicate items (items ## 2 and 1 are identical)', + {i: 1, j: 2}); + + var expectedErrors = _ajv._opts.allErrors ? 2 : 1; + shouldBeInvalid(validate, [1, "2", "2", 2], expectedErrors); + testTypeError(0, '/1'); + if (expectedErrors == 2) testTypeError(1, '/2'); + + function testTypeError(i, dataPath) { + var err = validate.errors[i]; + shouldBeError(err, 'type', '#/items/type', dataPath, 'should be number'); + } + }); + }); }); diff --git a/spec/extras.spec.js b/spec/extras.spec.js index dcf1713e2..15c51a6bc 100644 --- a/spec/extras.spec.js +++ b/spec/extras.spec.js @@ -8,7 +8,6 @@ var jsonSchemaTest = require('json-schema-test') var instances = getAjvInstances(options, { $data: true, - patternGroups: true, unknownFormats: ['allowedUnknown'] }); diff --git a/spec/extras/patternGroups.json b/spec/extras/patternGroups.json deleted file mode 100644 index 94eea5ba1..000000000 --- a/spec/extras/patternGroups.json +++ /dev/null @@ -1,271 +0,0 @@ -[ - { - "description": "patternGroups validates properties matching a regex (equivalent to the test from draft 4)", - "schema": { - "patternGroups": { - "f.*o": { - "schema": {"type": "integer"} - } - } - }, - "tests": [ - { - "description": "a single valid match is valid", - "data": {"foo": 1}, - "valid": true - }, - { - "description": "multiple valid matches is valid", - "data": {"foo": 1, "foooooo" : 2}, - "valid": true - }, - { - "description": "a single invalid match is invalid", - "data": {"foo": "bar", "fooooo": 2}, - "valid": false - }, - { - "description": "multiple invalid matches is invalid", - "data": {"foo": "bar", "foooooo" : "baz"}, - "valid": false - }, - { - "description": "ignores non-objects", - "data": 12, - "valid": true - } - ] - }, - { - "description": "multiple simultaneous patternGroups are validated (equivalent to the test from draft 4)", - "schema": { - "patternGroups": { - "a*": { - "schema": {"type": "integer"} - }, - "aaa*": { - "schema": {"maximum": 20} - } - } - }, - "tests": [ - { - "description": "a single valid match is valid", - "data": {"a": 21}, - "valid": true - }, - { - "description": "a simultaneous match is valid", - "data": {"aaaa": 18}, - "valid": true - }, - { - "description": "multiple matches is valid", - "data": {"a": 21, "aaaa": 18}, - "valid": true - }, - { - "description": "an invalid due to one is invalid", - "data": {"a": "bar"}, - "valid": false - }, - { - "description": "an invalid due to the other is invalid", - "data": {"aaaa": 31}, - "valid": false - }, - { - "description": "an invalid due to both is invalid", - "data": {"aaa": "foo", "aaaa": 31}, - "valid": false - } - ] - }, - { - "description": "regexes in patternGroups are not anchored by default and are case sensitive (equivalent to the test from draft 4)", - "schema": { - "patternGroups": { - "[0-9]{2,}": { - "schema": { "type": "boolean" } - }, - "X_": { - "schema": { "type": "string" } - } - } - }, - "tests": [ - { - "description": "non recognized members are ignored", - "data": { "answer 1": "42" }, - "valid": true - }, - { - "description": "recognized members are accounted for", - "data": { "a31b": null }, - "valid": false - }, - { - "description": "regexes are case sensitive", - "data": { "a_x_3": 3 }, - "valid": true - }, - { - "description": "regexes are case sensitive, 2", - "data": { "a_X_3": 3 }, - "valid": false - } - ] - }, - { - "description": - "patternGroups validates that the number of properties matching a regex is within limit", - "schema": { - "patternGroups": { - "f.*o": { - "schema": {"type": "integer"}, - "minimum": 1, - "maximum": 2 - } - } - }, - "tests": [ - { - "description": "a single valid match is valid", - "data": {"foo": 1}, - "valid": true - }, - { - "description": "2 valid matches are valid", - "data": {"foo": 1, "foooo" : 2}, - "valid": true - }, - { - "description": "no valid matches are invalid", - "data": {}, - "valid": false - }, - { - "description": "more than 2 valid matches are invalid", - "data": {"foo": 1, "foooo" : 2, "foooooo" : 3}, - "valid": false - }, - { - "description": "sinlge invalid match is invalid", - "data": {"foo": 1, "foooooo" : "baz"}, - "valid": false - } - ] - }, - { - "description": "multiple simultaneous patternGroups are validated for number of matching properties", - "schema": { - "patternGroups": { - "a*": { - "schema": {"type": "integer"}, - "minimum": 1 - }, - "aaa*": { - "schema": {"maximum": 20}, - "maximum": 1 - } - } - }, - "tests": [ - { - "description": "a single first match is valid", - "data": {"a": 21}, - "valid": true - }, - { - "description": "no first match is invalid", - "data": {}, - "valid": false - }, - { - "description": "simultaneous match is valid", - "data": {"aaaa": 18}, - "valid": true - }, - { - "description": "multiple matches is valid", - "data": {"a": 21, "aaaa": 18}, - "valid": true - }, - { - "description": "two second matches are invalid", - "data": {"aaa": 17, "aaaa": 18}, - "valid": false - }, - { - "description": "invalid due to the first is invalid", - "data": {"a": "bar"}, - "valid": false - }, - { - "description": "invalid due to the second is invalid", - "data": {"a": 21, "aaaa": 31}, - "valid": false - }, - { - "description": "invalid due to both is invalid", - "data": {"a": "foo", "aaaa": 31}, - "valid": false - } - ] - }, - { - "description": "properties, patternGroups, additionalProperties interaction (equivalent to the test from draft 4)", - "schema": { - "properties": { - "foo": {"type": "array", "maxItems": 3}, - "bar": {"type": "array"} - }, - "patternGroups": { - "f.o": { "schema": {"minItems": 2} } - }, - "additionalProperties": {"type": "integer"} - }, - "tests": [ - { - "description": "property validates property", - "data": {"foo": [1, 2]}, - "valid": true - }, - { - "description": "property invalidates property", - "data": {"foo": [1, 2, 3, 4]}, - "valid": false - }, - { - "description": "patternGroups invalidates property", - "data": {"foo": []}, - "valid": false - }, - { - "description": "patternGroups validates nonproperty", - "data": {"fxo": [1, 2]}, - "valid": true - }, - { - "description": "patternGroups invalidates nonproperty", - "data": {"fxo": []}, - "valid": false - }, - { - "description": "additionalProperty ignores property", - "data": {"bar": []}, - "valid": true - }, - { - "description": "additionalProperty validates others", - "data": {"quux": 3}, - "valid": true - }, - { - "description": "additionalProperty invalidates others", - "data": {"quux": "foo"}, - "valid": false - } - ] - } -] diff --git a/spec/issues.spec.js b/spec/issues.spec.js deleted file mode 100644 index e0ba9fc60..000000000 --- a/spec/issues.spec.js +++ /dev/null @@ -1,627 +0,0 @@ -'use strict'; - -var Ajv = require('./ajv') - , should = require('./chai').should(); - - -describe('issue #8: schema with shared references', function() { - it('should be supported by addSchema', spec('addSchema')); - - it('should be supported by compile', spec('compile')); - - function spec(method) { - return function() { - var ajv = new Ajv; - - var propertySchema = { - type: 'string', - maxLength: 4 - }; - - var schema = { - id: 'obj.json#', - type: 'object', - properties: { - foo: propertySchema, - bar: propertySchema - } - }; - - ajv[method](schema); - - var result = ajv.validate('obj.json#', { foo: 'abc', bar: 'def' }); - result .should.equal(true); - - result = ajv.validate('obj.json#', { foo: 'abcde', bar: 'fghg' }); - result .should.equal(false); - ajv.errors .should.have.length(1); - }; - } -}); - -describe('issue #50: references with "definitions"', function () { - it('should be supported by addSchema', spec('addSchema')); - - it('should be supported by compile', spec('addSchema')); - - function spec(method) { - return function() { - var result; - - var ajv = new Ajv; - - ajv[method]({ - id: 'http://example.com/test/person.json#', - definitions: { - name: { type: 'string' } - }, - type: 'object', - properties: { - name: { $ref: '#/definitions/name'} - } - }); - - ajv[method]({ - id: 'http://example.com/test/employee.json#', - type: 'object', - properties: { - person: { $ref: '/test/person.json#' }, - role: { type: 'string' } - } - }); - - result = ajv.validate('http://example.com/test/employee.json#', { - person: { - name: 'Alice' - }, - role: 'Programmer' - }); - - result. should.equal(true); - should.equal(ajv.errors, null); - }; - } -}); - - -describe('issue #182, NaN validation', function() { - it('should not pass minimum/maximum validation', function() { - testNaN({ minimum: 1 }, false); - testNaN({ maximum: 1 }, false); - }); - - it('should pass type: number validation', function() { - testNaN({ type: 'number' }, true); - }); - - it('should not pass type: integer validation', function() { - testNaN({ type: 'integer' }, false); - }); - - function testNaN(schema, NaNisValid) { - var ajv = new Ajv; - var validate = ajv.compile(schema); - validate(NaN) .should.equal(NaNisValid); - } -}); - - -describe('issue #204, options schemas and $data used together', function() { - it('should use v5 metaschemas by default', function() { - var ajv = new Ajv({ - schemas: [{id: 'str', type: 'string'}], - $data: true - }); - - var schema = { const: 42 }; - var validate = ajv.compile(schema); - - validate(42) .should.equal(true); - validate(43) .should.equal(false); - - ajv.validate('str', 'foo') .should.equal(true); - ajv.validate('str', 42) .should.equal(false); - }); -}); - - -describe('issue #181, custom keyword is not validated in allErrors mode if there were previous error', function() { - it('should validate custom keyword that doesn\'t create errors', function() { - testCustomKeywordErrors({ - type:'object', - errors: true, - validate: function v(/* value */) { - return false; - } - }); - }); - - it('should validate custom keyword that creates errors', function() { - testCustomKeywordErrors({ - type:'object', - errors: true, - validate: function v(/* value */) { - v.errors = v.errors || []; - v.errors.push({ - keyword: 'alwaysFails', - message: 'alwaysFails error', - params: { - keyword: 'alwaysFails' - } - }); - - return false; - } - }); - }); - - function testCustomKeywordErrors(def) { - var ajv = new Ajv({ allErrors: true }); - - ajv.addKeyword('alwaysFails', def); - - var schema = { - required: ['foo'], - alwaysFails: true - }; - - var validate = ajv.compile(schema); - - validate({ foo: 1 }) .should.equal(false); - validate.errors .should.have.length(1); - validate.errors[0].keyword .should.equal('alwaysFails'); - - validate({}) .should.equal(false); - validate.errors .should.have.length(2); - validate.errors[0].keyword .should.equal('required'); - validate.errors[1].keyword .should.equal('alwaysFails'); - } -}); - - -describe('issue #210, mutual recursive $refs that are schema fragments', function() { - it('should compile and validate schema when one ref is fragment', function() { - var ajv = new Ajv; - - ajv.addSchema({ - "id" : "foo", - "definitions": { - "bar": { - "properties": { - "baz": { - "anyOf": [ - { "enum": [42] }, - { "$ref": "boo" } - ] - } - } - } - } - }); - - ajv.addSchema({ - "id" : "boo", - "type": "object", - "required": ["quux"], - "properties": { - "quux": { "$ref": "foo#/definitions/bar" } - } - }); - - var validate = ajv.compile({ "$ref": "foo#/definitions/bar" }); - - validate({ baz: { quux: { baz: 42 } } }) .should.equal(true); - validate({ baz: { quux: { baz: "foo" } } }) .should.equal(false); - }); - - it('should compile and validate schema when both refs are fragments', function() { - var ajv = new Ajv; - - ajv.addSchema({ - "id" : "foo", - "definitions": { - "bar": { - "properties": { - "baz": { - "anyOf": [ - { "enum": [42] }, - { "$ref": "boo#/definitions/buu" } - ] - } - } - } - } - }); - - ajv.addSchema({ - "id" : "boo", - "definitions": { - "buu": { - "type": "object", - "required": ["quux"], - "properties": { - "quux": { "$ref": "foo#/definitions/bar" } - } - } - } - }); - - var validate = ajv.compile({ "$ref": "foo#/definitions/bar" }); - - validate({ baz: { quux: { baz: 42 } } }) .should.equal(true); - validate({ baz: { quux: { baz: "foo" } } }) .should.equal(false); - }); -}); - - -describe('issue #240, mutually recursive fragment refs reference a common schema', function() { - var apiSchema = { - $schema: 'http://json-schema.org/draft-06/schema#', - id: 'schema://api.schema#', - resource: { - id: '#resource', - properties: { - id: { type: 'string' } - } - }, - resourceIdentifier: { - id: '#resource_identifier', - properties: { - id: { type: 'string' }, - type: { type: 'string' } - } - } - }; - - var domainSchema = { - $schema: 'http://json-schema.org/draft-06/schema#', - id: 'schema://domain.schema#', - properties: { - data: { - oneOf: [ - { $ref: 'schema://library.schema#resource_identifier' }, - { $ref: 'schema://catalog_item.schema#resource_identifier' }, - ] - } - } - }; - - it('should compile and validate schema when one ref is fragment', function() { - var ajv = new Ajv; - - var librarySchema = { - $schema: 'http://json-schema.org/draft-06/schema#', - id: 'schema://library.schema#', - properties: { - name: { type: 'string' }, - links: { - properties: { - catalogItems: { - type: 'array', - items: { $ref: 'schema://catalog_item_resource_identifier.schema#' } - } - } - } - }, - definitions: { - resource_identifier: { - id: '#resource_identifier', - allOf: [ - { - properties: { - type: { - type: 'string', - 'enum': ['Library'] - } - } - }, - { $ref: 'schema://api.schema#resource_identifier' } - ] - } - } - }; - - var catalogItemSchema = { - $schema: 'http://json-schema.org/draft-06/schema#', - id: 'schema://catalog_item.schema#', - properties: { - name: { type: 'string' }, - links: { - properties: { - library: { $ref: 'schema://library.schema#resource_identifier' } - } - } - }, - definitions: { - resource_identifier: { - id: '#resource_identifier', - allOf: [ - { - properties: { - type: { - type: 'string', - 'enum': ['CatalogItem'] - } - } - }, - { $ref: 'schema://api.schema#resource_identifier' } - ] - } - } - }; - - var catalogItemResourceIdentifierSchema = { - $schema: 'http://json-schema.org/draft-06/schema#', - id: 'schema://catalog_item_resource_identifier.schema#', - allOf: [ - { - properties: { - type: { - type: 'string', - enum: ['CatalogItem'] - } - } - }, - { - $ref: 'schema://api.schema#resource_identifier' - } - ] - }; - - ajv.addSchema(librarySchema); - ajv.addSchema(catalogItemSchema); - ajv.addSchema(catalogItemResourceIdentifierSchema); - ajv.addSchema(apiSchema); - - var validate = ajv.compile(domainSchema); - testSchema(validate); - }); - - it('should compile and validate schema when both refs are fragments', function() { - var ajv = new Ajv; - - var librarySchema = { - $schema: 'http://json-schema.org/draft-06/schema#', - id: 'schema://library.schema#', - properties: { - name: { type: 'string' }, - links: { - properties: { - catalogItems: { - type: 'array', - items: { $ref: 'schema://catalog_item.schema#resource_identifier' } - } - } - } - }, - definitions: { - resource_identifier: { - id: '#resource_identifier', - allOf: [ - { - properties: { - type: { - type: 'string', - 'enum': ['Library'] - } - } - }, - { $ref: 'schema://api.schema#resource_identifier' } - ] - } - } - }; - - var catalogItemSchema = { - $schema: 'http://json-schema.org/draft-06/schema#', - id: 'schema://catalog_item.schema#', - properties: { - name: { type: 'string' }, - links: { - properties: { - library: { $ref: 'schema://library.schema#resource_identifier' } - } - } - }, - definitions: { - resource_identifier: { - id: '#resource_identifier', - allOf: [ - { - properties: { - type: { - type: 'string', - 'enum': ['CatalogItem'] - } - } - }, - { $ref: 'schema://api.schema#resource_identifier' } - ] - } - } - }; - - ajv.addSchema(librarySchema); - ajv.addSchema(catalogItemSchema); - ajv.addSchema(apiSchema); - - var validate = ajv.compile(domainSchema); - testSchema(validate); - }); - - - function testSchema(validate) { - validate({ data: { type: 'Library', id: '123' } }) .should.equal(true); - validate({ data: { type: 'Library', id: 123 } }) .should.equal(false); - validate({ data: { type: 'CatalogItem', id: '123' } }) .should.equal(true); - validate({ data: { type: 'CatalogItem', id: 123 } }) .should.equal(false); - validate({ data: { type: 'Foo', id: '123' } }) .should.equal(false); - } -}); - - -describe('issue #259, support validating [meta-]schemas against themselves', function() { - it('should add schema before validation if "id" is the same as "$schema"', function() { - var ajv = new Ajv; - ajv.addMetaSchema(require('../lib/refs/json-schema-draft-04.json')); - var hyperSchema = require('./remotes/hyper-schema.json'); - ajv.addMetaSchema(hyperSchema); - }); -}); - - -describe.skip('issue #273, schemaPath in error in referenced schema', function() { - it('should have canonic reference with hash after file name', function() { - test(new Ajv); - test(new Ajv({inlineRefs: false})); - - function test(ajv) { - var schema = { - "properties": { - "a": { "$ref": "int" } - } - }; - - var referencedSchema = { - "id": "int", - "type": "integer" - }; - - ajv.addSchema(referencedSchema); - var validate = ajv.compile(schema); - - validate({ "a": "foo" }) .should.equal(false); - validate.errors[0].schemaPath .should.equal('int#/type'); - } - }); -}); - - -describe('issue #342, support uniqueItems with some non-JSON objects', function() { - var validate; - - before(function() { - var ajv = new Ajv; - validate = ajv.compile({ uniqueItems: true }); - }); - - it('should allow different RegExps', function() { - validate([/foo/, /bar/]) .should.equal(true); - validate([/foo/ig, /foo/gi]) .should.equal(false); - validate([/foo/, {}]) .should.equal(true); - }); - - it('should allow different Dates', function() { - validate([new Date('2016-11-11'), new Date('2016-11-12')]) .should.equal(true); - validate([new Date('2016-11-11'), new Date('2016-11-11')]) .should.equal(false); - validate([new Date('2016-11-11'), {}]) .should.equal(true); - }); - - it('should allow undefined properties', function() { - validate([{}, {foo: undefined}]) .should.equal(true); - validate([{foo: undefined}, {}]) .should.equal(true); - validate([{foo: undefined}, {bar: undefined}]) .should.equal(true); - validate([{foo: undefined}, {foo: undefined}]) .should.equal(false); - }); -}); - - -describe('issue #388, code clean-up not working', function() { - it('should remove assignement to rootData if it is not used', function() { - var ajv = new Ajv; - var validate = ajv.compile({ - type: 'object', - properties: { - foo: { type: 'string' } - } - }); - var code = validate.toString(); - code.match(/rootData/g).length .should.equal(1); - }); - - it('should remove assignement to errors if they are not used', function() { - var ajv = new Ajv; - var validate = ajv.compile({ - type: 'object' - }); - var code = validate.toString(); - should.equal(code.match(/[^.]errors|vErrors/g), null); - }); -}); - - -describe('issue #485, order of type validation', function() { - it('should validate types befor keywords', function() { - var ajv = new Ajv({allErrors: true}); - var validate = ajv.compile({ - type: ['integer', 'string'], - required: ['foo'], - minimum: 2 - }); - - validate(2) .should.equal(true); - validate('foo') .should.equal(true); - - validate(1.5) .should.equal(false); - checkErrors(['type', 'minimum']); - - validate({}) .should.equal(false); - checkErrors(['type', 'required']); - - function checkErrors(expectedErrs) { - validate.errors .should.have.length(expectedErrs.length); - expectedErrs.forEach(function (keyword, i) { - validate.errors[i].keyword .should.equal(keyword); - }); - } - }); -}); - - -describe('issue #521, incorrect warning with "id" property', function() { - it('should not log warning', function() { - var ajv = new Ajv({schemaId: '$id'}); - var consoleWarn = console.warn; - console.warn = function() { - throw new Error('should not log warning'); - }; - - try { - ajv.compile({ - "$id": "http://example.com/schema.json", - "type": "object", - "properties": { - "id": {"type": "string"}, - }, - "required": [ "id"] - }); - } finally { - console.warn = consoleWarn; - } - }); -}); - - -describe('issue #533, throwing missing ref exception with option missingRefs: "ignore"', function() { - var schema = { - "type": "object", - "properties": { - "foo": {"$ref": "#/definitions/missing"}, - "bar": {"$ref": "#/definitions/missing"} - } - }; - - it('should pass validation without throwing exception', function() { - var ajv = new Ajv({missingRefs: 'ignore'}); - var validate = ajv.compile(schema); - validate({foo: 'anything'}) .should.equal(true); - validate({foo: 'anything', bar: 'whatever'}) .should.equal(true); - }); - - it('should throw exception during schema compilation with option missingRefs: true', function() { - var ajv = new Ajv; - should.throw(function() { - ajv.compile(schema); - }); - }); -}); diff --git a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js new file mode 100644 index 000000000..bc3d0d7d0 --- /dev/null +++ b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js @@ -0,0 +1,20 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #1001: addKeyword breaks schema without ID', function() { + it('should allow using schemas without ID with addKeyword', function() { + var schema = { + definitions: { + foo: {} + } + }; + + var ajv = new Ajv(); + ajv.addSchema(schema); + ajv.addKeyword('myKeyword', {}); + ajv.getSchema('#/definitions/foo') .should.be.a('function'); + }); +}); diff --git a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js new file mode 100644 index 000000000..aa734aa15 --- /dev/null +++ b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js @@ -0,0 +1,58 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #181, custom keyword is not validated in allErrors mode if there were previous error', function() { + it('should validate custom keyword that doesn\'t create errors', function() { + testCustomKeywordErrors({ + type:'object', + errors: true, + validate: function v(/* value */) { + return false; + } + }); + }); + + it('should validate custom keyword that creates errors', function() { + testCustomKeywordErrors({ + type:'object', + errors: true, + validate: function v(/* value */) { + v.errors = v.errors || []; + v.errors.push({ + keyword: 'alwaysFails', + message: 'alwaysFails error', + params: { + keyword: 'alwaysFails' + } + }); + + return false; + } + }); + }); + + function testCustomKeywordErrors(def) { + var ajv = new Ajv({ allErrors: true }); + + ajv.addKeyword('alwaysFails', def); + + var schema = { + required: ['foo'], + alwaysFails: true + }; + + var validate = ajv.compile(schema); + + validate({ foo: 1 }) .should.equal(false); + validate.errors .should.have.length(1); + validate.errors[0].keyword .should.equal('alwaysFails'); + + validate({}) .should.equal(false); + validate.errors .should.have.length(2); + validate.errors[0].keyword .should.equal('required'); + validate.errors[1].keyword .should.equal('alwaysFails'); + } +}); diff --git a/spec/issues/182_nan_validation.spec.js b/spec/issues/182_nan_validation.spec.js new file mode 100644 index 000000000..4d341a3c9 --- /dev/null +++ b/spec/issues/182_nan_validation.spec.js @@ -0,0 +1,26 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #182, NaN validation', function() { + it('should not pass minimum/maximum validation', function() { + testNaN({ minimum: 1 }, false); + testNaN({ maximum: 1 }, false); + }); + + it('should pass type: number validation', function() { + testNaN({ type: 'number' }, true); + }); + + it('should not pass type: integer validation', function() { + testNaN({ type: 'integer' }, false); + }); + + function testNaN(schema, NaNisValid) { + var ajv = new Ajv; + var validate = ajv.compile(schema); + validate(NaN) .should.equal(NaNisValid); + } +}); diff --git a/spec/issues/204_options_schemas_data_together.spec.js b/spec/issues/204_options_schemas_data_together.spec.js new file mode 100644 index 000000000..73746c17e --- /dev/null +++ b/spec/issues/204_options_schemas_data_together.spec.js @@ -0,0 +1,23 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #204, options schemas and $data used together', function() { + it('should use v5 metaschemas by default', function() { + var ajv = new Ajv({ + schemas: [{$id: 'str', type: 'string'}], + $data: true + }); + + var schema = { const: 42 }; + var validate = ajv.compile(schema); + + validate(42) .should.equal(true); + validate(43) .should.equal(false); + + ajv.validate('str', 'foo') .should.equal(true); + ajv.validate('str', 42) .should.equal(false); + }); +}); diff --git a/spec/issues/210_mutual_recur_frags.spec.js b/spec/issues/210_mutual_recur_frags.spec.js new file mode 100644 index 000000000..58847f017 --- /dev/null +++ b/spec/issues/210_mutual_recur_frags.spec.js @@ -0,0 +1,79 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #210, mutual recursive $refs that are schema fragments', function() { + it('should compile and validate schema when one ref is fragment', function() { + var ajv = new Ajv; + + ajv.addSchema({ + "$id" : "foo", + "definitions": { + "bar": { + "properties": { + "baz": { + "anyOf": [ + { "enum": [42] }, + { "$ref": "boo" } + ] + } + } + } + } + }); + + ajv.addSchema({ + "$id" : "boo", + "type": "object", + "required": ["quux"], + "properties": { + "quux": { "$ref": "foo#/definitions/bar" } + } + }); + + var validate = ajv.compile({ "$ref": "foo#/definitions/bar" }); + + validate({ baz: { quux: { baz: 42 } } }) .should.equal(true); + validate({ baz: { quux: { baz: "foo" } } }) .should.equal(false); + }); + + it('should compile and validate schema when both refs are fragments', function() { + var ajv = new Ajv; + + ajv.addSchema({ + "$id" : "foo", + "definitions": { + "bar": { + "properties": { + "baz": { + "anyOf": [ + { "enum": [42] }, + { "$ref": "boo#/definitions/buu" } + ] + } + } + } + } + }); + + ajv.addSchema({ + "$id" : "boo", + "definitions": { + "buu": { + "type": "object", + "required": ["quux"], + "properties": { + "quux": { "$ref": "foo#/definitions/bar" } + } + } + } + }); + + var validate = ajv.compile({ "$ref": "foo#/definitions/bar" }); + + validate({ baz: { quux: { baz: 42 } } }) .should.equal(true); + validate({ baz: { quux: { baz: "foo" } } }) .should.equal(false); + }); +}); diff --git a/spec/issues/240_mutual_recur_frags_common_ref.spec.js b/spec/issues/240_mutual_recur_frags_common_ref.spec.js new file mode 100644 index 000000000..d9e40241d --- /dev/null +++ b/spec/issues/240_mutual_recur_frags_common_ref.spec.js @@ -0,0 +1,210 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #240, mutually recursive fragment refs reference a common schema', function() { + var apiSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'schema://api.schema#', + resource: { + $id: '#resource', + properties: { + id: { type: 'string' } + } + }, + resourceIdentifier: { + $id: '#resource_identifier', + properties: { + id: { type: 'string' }, + type: { type: 'string' } + } + } + }; + + var domainSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'schema://domain.schema#', + properties: { + data: { + oneOf: [ + { $ref: 'schema://library.schema#resource_identifier' }, + { $ref: 'schema://catalog_item.schema#resource_identifier' }, + ] + } + } + }; + + it('should compile and validate schema when one ref is fragment', function() { + var ajv = new Ajv; + + var librarySchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'schema://library.schema#', + properties: { + name: { type: 'string' }, + links: { + properties: { + catalogItems: { + type: 'array', + items: { $ref: 'schema://catalog_item_resource_identifier.schema#' } + } + } + } + }, + definitions: { + resource_identifier: { + $id: '#resource_identifier', + allOf: [ + { + properties: { + type: { + type: 'string', + 'enum': ['Library'] + } + } + }, + { $ref: 'schema://api.schema#resource_identifier' } + ] + } + } + }; + + var catalogItemSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'schema://catalog_item.schema#', + properties: { + name: { type: 'string' }, + links: { + properties: { + library: { $ref: 'schema://library.schema#resource_identifier' } + } + } + }, + definitions: { + resource_identifier: { + $id: '#resource_identifier', + allOf: [ + { + properties: { + type: { + type: 'string', + 'enum': ['CatalogItem'] + } + } + }, + { $ref: 'schema://api.schema#resource_identifier' } + ] + } + } + }; + + var catalogItemResourceIdentifierSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'schema://catalog_item_resource_identifier.schema#', + allOf: [ + { + properties: { + type: { + type: 'string', + enum: ['CatalogItem'] + } + } + }, + { + $ref: 'schema://api.schema#resource_identifier' + } + ] + }; + + ajv.addSchema(librarySchema); + ajv.addSchema(catalogItemSchema); + ajv.addSchema(catalogItemResourceIdentifierSchema); + ajv.addSchema(apiSchema); + + var validate = ajv.compile(domainSchema); + testSchema(validate); + }); + + it('should compile and validate schema when both refs are fragments', function() { + var ajv = new Ajv; + + var librarySchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'schema://library.schema#', + properties: { + name: { type: 'string' }, + links: { + properties: { + catalogItems: { + type: 'array', + items: { $ref: 'schema://catalog_item.schema#resource_identifier' } + } + } + } + }, + definitions: { + resource_identifier: { + $id: '#resource_identifier', + allOf: [ + { + properties: { + type: { + type: 'string', + 'enum': ['Library'] + } + } + }, + { $ref: 'schema://api.schema#resource_identifier' } + ] + } + } + }; + + var catalogItemSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'schema://catalog_item.schema#', + properties: { + name: { type: 'string' }, + links: { + properties: { + library: { $ref: 'schema://library.schema#resource_identifier' } + } + } + }, + definitions: { + resource_identifier: { + $id: '#resource_identifier', + allOf: [ + { + properties: { + type: { + type: 'string', + 'enum': ['CatalogItem'] + } + } + }, + { $ref: 'schema://api.schema#resource_identifier' } + ] + } + } + }; + + ajv.addSchema(librarySchema); + ajv.addSchema(catalogItemSchema); + ajv.addSchema(apiSchema); + + var validate = ajv.compile(domainSchema); + testSchema(validate); + }); + + + function testSchema(validate) { + validate({ data: { type: 'Library', id: '123' } }) .should.equal(true); + validate({ data: { type: 'Library', id: 123 } }) .should.equal(false); + validate({ data: { type: 'CatalogItem', id: '123' } }) .should.equal(true); + validate({ data: { type: 'CatalogItem', id: 123 } }) .should.equal(false); + validate({ data: { type: 'Foo', id: '123' } }) .should.equal(false); + } +}); diff --git a/spec/issues/259_validate_meta_against_itself.spec.js b/spec/issues/259_validate_meta_against_itself.spec.js new file mode 100644 index 000000000..d99fc21ef --- /dev/null +++ b/spec/issues/259_validate_meta_against_itself.spec.js @@ -0,0 +1,13 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #259, support validating [meta-]schemas against themselves', function() { + it('should add schema before validation if "id" is the same as "$schema"', function() { + var ajv = new Ajv; + var hyperSchema = require('../remotes/hyper-schema.json'); + ajv.addMetaSchema(hyperSchema); + }); +}); diff --git a/spec/issues/273_error_schemaPath_refd_schema.spec.js b/spec/issues/273_error_schemaPath_refd_schema.spec.js new file mode 100644 index 000000000..927164087 --- /dev/null +++ b/spec/issues/273_error_schemaPath_refd_schema.spec.js @@ -0,0 +1,31 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe.skip('issue #273, schemaPath in error in referenced schema', function() { + it('should have canonic reference with hash after file name', function() { + test(new Ajv); + test(new Ajv({inlineRefs: false})); + + function test(ajv) { + var schema = { + "properties": { + "a": { "$ref": "int" } + } + }; + + var referencedSchema = { + "id": "int", + "type": "integer" + }; + + ajv.addSchema(referencedSchema); + var validate = ajv.compile(schema); + + validate({ "a": "foo" }) .should.equal(false); + validate.errors[0].schemaPath .should.equal('int#/type'); + } + }); +}); diff --git a/spec/issues/342_uniqueItems_non-json_objects.spec.js b/spec/issues/342_uniqueItems_non-json_objects.spec.js new file mode 100644 index 000000000..ae7eee0cf --- /dev/null +++ b/spec/issues/342_uniqueItems_non-json_objects.spec.js @@ -0,0 +1,33 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #342, support uniqueItems with some non-JSON objects', function() { + var validate; + + before(function() { + var ajv = new Ajv; + validate = ajv.compile({ uniqueItems: true }); + }); + + it('should allow different RegExps', function() { + validate([/foo/, /bar/]) .should.equal(true); + validate([/foo/ig, /foo/gi]) .should.equal(false); + validate([/foo/, {}]) .should.equal(true); + }); + + it('should allow different Dates', function() { + validate([new Date('2016-11-11'), new Date('2016-11-12')]) .should.equal(true); + validate([new Date('2016-11-11'), new Date('2016-11-11')]) .should.equal(false); + validate([new Date('2016-11-11'), {}]) .should.equal(true); + }); + + it('should allow undefined properties', function() { + validate([{}, {foo: undefined}]) .should.equal(true); + validate([{foo: undefined}, {}]) .should.equal(true); + validate([{foo: undefined}, {bar: undefined}]) .should.equal(true); + validate([{foo: undefined}, {foo: undefined}]) .should.equal(false); + }); +}); diff --git a/spec/issues/485_type_validation_priority.spec.js b/spec/issues/485_type_validation_priority.spec.js new file mode 100644 index 000000000..eb2b1e9df --- /dev/null +++ b/spec/issues/485_type_validation_priority.spec.js @@ -0,0 +1,32 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #485, order of type validation', function() { + it('should validate types before keywords', function() { + var ajv = new Ajv({allErrors: true}); + var validate = ajv.compile({ + type: ['integer', 'string'], + required: ['foo'], + minimum: 2 + }); + + validate(2) .should.equal(true); + validate('foo') .should.equal(true); + + validate(1.5) .should.equal(false); + checkErrors(['type', 'minimum']); + + validate({}) .should.equal(false); + checkErrors(['type', 'required']); + + function checkErrors(expectedErrs) { + validate.errors .should.have.length(expectedErrs.length); + expectedErrs.forEach(function (keyword, i) { + validate.errors[i].keyword .should.equal(keyword); + }); + } + }); +}); diff --git a/spec/issues/50_refs_with_definitions.spec.js b/spec/issues/50_refs_with_definitions.spec.js new file mode 100644 index 000000000..26b84a8cc --- /dev/null +++ b/spec/issues/50_refs_with_definitions.spec.js @@ -0,0 +1,49 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('issue #50: references with "definitions"', function () { + it('should be supported by addSchema', spec('addSchema')); + + it('should be supported by compile', spec('addSchema')); + + function spec(method) { + return function() { + var result; + + var ajv = new Ajv; + + ajv[method]({ + $id: 'http://example.com/test/person.json#', + definitions: { + name: { type: 'string' } + }, + type: 'object', + properties: { + name: { $ref: '#/definitions/name'} + } + }); + + ajv[method]({ + $id: 'http://example.com/test/employee.json#', + type: 'object', + properties: { + person: { $ref: '/test/person.json#' }, + role: { type: 'string' } + } + }); + + result = ajv.validate('http://example.com/test/employee.json#', { + person: { + name: 'Alice' + }, + role: 'Programmer' + }); + + result. should.equal(true); + should.equal(ajv.errors, null); + }; + } +}); diff --git a/spec/issues/521_wrong_warning_id_property.spec.js b/spec/issues/521_wrong_warning_id_property.spec.js new file mode 100644 index 000000000..79fbbce7e --- /dev/null +++ b/spec/issues/521_wrong_warning_id_property.spec.js @@ -0,0 +1,28 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #521, incorrect warning with "id" property', function() { + it('should not log warning', function() { + var ajv = new Ajv({schemaId: '$id'}); + var consoleWarn = console.warn; + console.warn = function() { + throw new Error('should not log warning'); + }; + + try { + ajv.compile({ + "$id": "http://example.com/schema.json", + "type": "object", + "properties": { + "id": {"type": "string"}, + }, + "required": [ "id"] + }); + } finally { + console.warn = consoleWarn; + } + }); +}); diff --git a/spec/issues/533_missing_ref_error_when_ignore.spec.js b/spec/issues/533_missing_ref_error_when_ignore.spec.js new file mode 100644 index 000000000..4e77d5fc6 --- /dev/null +++ b/spec/issues/533_missing_ref_error_when_ignore.spec.js @@ -0,0 +1,29 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('issue #533, throwing missing ref exception with option missingRefs: "ignore"', function() { + var schema = { + "type": "object", + "properties": { + "foo": {"$ref": "#/definitions/missing"}, + "bar": {"$ref": "#/definitions/missing"} + } + }; + + it('should pass validation without throwing exception', function() { + var ajv = new Ajv({missingRefs: 'ignore'}); + var validate = ajv.compile(schema); + validate({foo: 'anything'}) .should.equal(true); + validate({foo: 'anything', bar: 'whatever'}) .should.equal(true); + }); + + it('should throw exception during schema compilation with option missingRefs: true', function() { + var ajv = new Ajv; + should.throw(function() { + ajv.compile(schema); + }); + }); +}); diff --git a/spec/issues/617_full_format_leap_year.spec.js b/spec/issues/617_full_format_leap_year.spec.js new file mode 100644 index 000000000..995c4ba13 --- /dev/null +++ b/spec/issues/617_full_format_leap_year.spec.js @@ -0,0 +1,47 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('PR #617, full date format validation should understand leap years', function () { + it('should handle non leap year affected dates with date-time', function() { + var ajv = new Ajv({ format: 'full' }); + + var schema = { format: 'date-time' }; + var validDateTime = '2016-01-31T00:00:00Z'; + + ajv.validate(schema, validDateTime).should.equal(true); + }); + + it('should handle non leap year affected dates with date', function () { + var ajv = new Ajv({ format: 'full' }); + + var schema = { format: 'date' }; + var validDate = '2016-11-30'; + + ajv.validate(schema, validDate).should.equal(true); + }); + + it('should handle year leaps as date-time', function() { + var ajv = new Ajv({ format: 'full' }); + + var schema = { format: 'date-time' }; + var validDateTime = '2016-02-29T00:00:00Z'; + var invalidDateTime = '2017-02-29T00:00:00Z'; + + ajv.validate(schema, validDateTime) .should.equal(true); + ajv.validate(schema, invalidDateTime) .should.equal(false); + }); + + it('should handle year leaps as date', function() { + var ajv = new Ajv({ format: 'full' }); + + var schema = { format: 'date' }; + var validDate = '2016-02-29'; + var invalidDate = '2017-02-29'; + + ajv.validate(schema, validDate) .should.equal(true); + ajv.validate(schema, invalidDate) .should.equal(false); + }); +}); diff --git a/spec/issues/743_removeAdditional_to_remove_proto.spec.js b/spec/issues/743_removeAdditional_to_remove_proto.spec.js new file mode 100644 index 000000000..f5a01926d --- /dev/null +++ b/spec/issues/743_removeAdditional_to_remove_proto.spec.js @@ -0,0 +1,41 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #743, property __proto__ should be removed with removeAdditional option', function() { + it('should remove additional properties', function() { + var ajv = new Ajv({removeAdditional: true}); + + var schema = { + properties: { + obj: { + additionalProperties: false, + properties: { + a: { type: 'string' }, + b: { type: 'string' }, + c: { type: 'string' }, + d: { type: 'string' }, + e: { type: 'string' }, + f: { type: 'string' }, + g: { type: 'string' }, + h: { type: 'string' }, + i: { type: 'string' } + } + } + } + }; + + var obj= Object.create(null); + obj.__proto__ = null; // should be removed + obj.additional = 'will be removed'; + obj.a = 'valid'; + obj.b = 'valid'; + + var data = {obj: obj}; + + ajv.validate(schema, data) .should.equal(true); + Object.keys(data.obj) .should.eql(['a', 'b']); + }); +}); diff --git a/spec/issues/768_passContext_recursive_ref.spec.js b/spec/issues/768_passContext_recursive_ref.spec.js new file mode 100644 index 000000000..3410f7462 --- /dev/null +++ b/spec/issues/768_passContext_recursive_ref.spec.js @@ -0,0 +1,114 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #768, fix passContext in recursive $ref', function() { + var ajv, contexts; + + beforeEach(function() { + contexts = []; + }); + + describe('passContext = true', function() { + it('should pass this value as context to custom keyword validation function', function() { + var validate = getValidate(true); + var self = {}; + validate.call(self, { bar: 'a', baz: { bar: 'b' } }); + contexts .should.have.length(2); + contexts.forEach(function(ctx) { + ctx .should.equal(self); + }); + }); + }); + + describe('passContext = false', function() { + it('should pass ajv instance as context to custom keyword validation function', function() { + var validate = getValidate(false); + validate({ bar: 'a', baz: { bar: 'b' } }); + contexts .should.have.length(2); + contexts.forEach(function(ctx) { + ctx .should.equal(ajv); + }); + }); + }); + + describe('ref is fragment and passContext = true', function() { + it('should pass this value as context to custom keyword validation function', function() { + var validate = getValidateFragments(true); + var self = {}; + validate.call(self, { baz: { corge: 'a', quux: { baz: { corge: 'b' } } } }); + contexts .should.have.length(2); + contexts.forEach(function(ctx) { + ctx .should.equal(self); + }); + }); + }); + + describe('ref is fragment and passContext = false', function() { + it('should pass ajv instance as context to custom keyword validation function', function() { + var validate = getValidateFragments(false); + validate({ baz: { corge: 'a', quux: { baz: { corge: 'b' } } } }); + contexts .should.have.length(2); + contexts.forEach(function(ctx) { + ctx .should.equal(ajv); + }); + }); + }); + + function getValidate(passContext) { + ajv = new Ajv({ passContext: passContext }); + ajv.addKeyword('testValidate', { validate: storeContext }); + + var schema = { + "$id" : "foo", + "type": "object", + "required": ["bar"], + "properties": { + "bar": { "testValidate": true }, + "baz": { + "$ref": "foo" + } + } + }; + + return ajv.compile(schema); + } + + + function getValidateFragments(passContext) { + ajv = new Ajv({ passContext: passContext }); + ajv.addKeyword('testValidate', { validate: storeContext }); + + ajv.addSchema({ + "$id" : "foo", + "definitions": { + "bar": { + "properties": { + "baz": { + "$ref": "boo" + } + } + } + } + }); + + ajv.addSchema({ + "$id" : "boo", + "type": "object", + "required": ["corge"], + "properties": { + "quux": { "$ref": "foo#/definitions/bar" }, + "corge": { "testValidate": true } + } + }); + + return ajv.compile({ "$ref": "foo#/definitions/bar" }); + } + + function storeContext() { + contexts.push(this); + return true; + } +}); diff --git a/spec/issues/8_shared_refs.spec.js b/spec/issues/8_shared_refs.spec.js new file mode 100644 index 000000000..2208f452e --- /dev/null +++ b/spec/issues/8_shared_refs.spec.js @@ -0,0 +1,40 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #8: schema with shared references', function() { + it('should be supported by addSchema', spec('addSchema')); + + it('should be supported by compile', spec('compile')); + + function spec(method) { + return function() { + var ajv = new Ajv; + + var propertySchema = { + type: 'string', + maxLength: 4 + }; + + var schema = { + $id: 'obj.json#', + type: 'object', + properties: { + foo: propertySchema, + bar: propertySchema + } + }; + + ajv[method](schema); + + var result = ajv.validate('obj.json#', { foo: 'abc', bar: 'def' }); + result .should.equal(true); + + result = ajv.validate('obj.json#', { foo: 'abcde', bar: 'fghg' }); + result .should.equal(false); + ajv.errors .should.have.length(1); + }; + } +}); diff --git a/spec/issues/955_removeAdditional_custom_keywords.spec.js b/spec/issues/955_removeAdditional_custom_keywords.spec.js new file mode 100644 index 000000000..4ef949d60 --- /dev/null +++ b/spec/issues/955_removeAdditional_custom_keywords.spec.js @@ -0,0 +1,48 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('issue #955: option removeAdditional breaks custom keywords', function() { + it('should support custom keywords with option removeAdditional', function() { + var ajv = new Ajv({removeAdditional: 'all'}); + + ajv.addKeyword('minTrimmedLength', { + type: 'string', + compile: function(schema) { + return function(str) { + return str.trim().length >= schema; + }; + }, + metaSchema: {type: 'integer'} + }); + + var schema = { + type: 'object', + properties: { + foo: { + type: 'string', + minTrimmedLength: 3 + } + }, + required: ['foo'] + }; + + var validate = ajv.compile(schema); + + var data = { + foo: ' bar ', + baz: '' + }; + validate(data) .should.equal(true); + data .should.not.have.property('baz'); + + data = { + foo: ' ba ', + baz: '' + }; + validate(data) .should.equal(false); + data .should.not.have.property('baz'); + }); +}); diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index bde31a2d9..48bce9618 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -10,38 +10,54 @@ var remoteRefs = { 'http://localhost:1234/integer.json': require('./JSON-Schema-Test-Suite/remotes/integer.json'), 'http://localhost:1234/subSchemas.json': require('./JSON-Schema-Test-Suite/remotes/subSchemas.json'), 'http://localhost:1234/folder/folderInteger.json': require('./JSON-Schema-Test-Suite/remotes/folder/folderInteger.json'), + 'http://localhost:1234/name.json': require('./JSON-Schema-Test-Suite/remotes/name.json') }; -runTest(getAjvInstances(options, {meta: false}), 4, typeof window == 'object' +var SKIP = { + 4: ['optional/zeroTerminatedFloats'], + 7: [ + 'optional/content', + 'format/idn-email', + 'format/idn-hostname', + 'format/iri', + 'format/iri-reference' + ] +}; + + +runTest(getAjvInstances(options, {meta: false, schemaId: 'id'}), 4, typeof window == 'object' ? suite(require('./JSON-Schema-Test-Suite/tests/draft4/{**/,}*.json', {mode: 'list'})) : './JSON-Schema-Test-Suite/tests/draft4/{**/,}*.json'); -runTest(getAjvInstances(options), 6, typeof window == 'object' +runTest(getAjvInstances(options, {meta: false}), 6, typeof window == 'object' ? suite(require('./JSON-Schema-Test-Suite/tests/draft6/{**/,}*.json', {mode: 'list'})) : './JSON-Schema-Test-Suite/tests/draft6/{**/,}*.json'); +runTest(getAjvInstances(options), 7, typeof window == 'object' + ? suite(require('./JSON-Schema-Test-Suite/tests/draft7/{**/,}*.json', {mode: 'list'})) + : './JSON-Schema-Test-Suite/tests/draft7/{**/,}*.json'); + function runTest(instances, draft, tests) { instances.forEach(function (ajv) { - ajv.addMetaSchema(require('../lib/refs/json-schema-draft-04.json')); - if (draft == 4) ajv._opts.defaultMeta = 'http://json-schema.org/draft-04/schema#'; + switch (draft) { + case 4: + ajv.addMetaSchema(require('../lib/refs/json-schema-draft-04.json')); + ajv._opts.defaultMeta = 'http://json-schema.org/draft-04/schema#'; + break; + case 6: + ajv.addMetaSchema(require('../lib/refs/json-schema-draft-06.json')); + ajv._opts.defaultMeta = 'http://json-schema.org/draft-06/schema#'; + break; + } for (var id in remoteRefs) ajv.addSchema(remoteRefs[id], id); }); jsonSchemaTest(instances, { description: 'JSON-Schema Test Suite draft-0' + draft + ': ' + instances.length + ' ajv instances with different options', suites: {tests: tests}, - only: [ - // 'type', 'not', 'allOf', 'anyOf', 'oneOf', 'enum', - // 'maximum', 'minimum', 'multipleOf', 'maxLength', 'minLength', 'pattern', - // 'properties', 'patternProperties', 'additionalProperties', - // 'dependencies', 'required', - // 'maxProperties', 'minProperties', 'maxItems', 'minItems', - // 'items', 'additionalItems', 'uniqueItems', - // 'optional/format', 'optional/bignum', - // 'ref', 'refRemote', 'definitions', - ], - skip: ['optional/zeroTerminatedFloats'], + only: [], + skip: SKIP[draft], assert: require('./chai').assert, afterError: after.error, afterEach: after.each, diff --git a/spec/options.spec.js b/spec/options.spec.js deleted file mode 100644 index dc58f4267..000000000 --- a/spec/options.spec.js +++ /dev/null @@ -1,1222 +0,0 @@ -'use strict'; - -var Ajv = require('./ajv') - , getAjvInstances = require('./ajv_instances') - , should = require('./chai').should(); - - -describe('Ajv Options', function () { - describe('removeAdditional', function() { - it('should remove all additional properties', function() { - var ajv = new Ajv({ removeAdditional: 'all' }); - - ajv.addSchema({ - id: '//test/fooBar', - properties: { foo: { type: 'string' }, bar: { type: 'string' } } - }); - - var object = { - foo: 'foo', bar: 'bar', baz: 'baz-to-be-removed' - }; - - ajv.validate('//test/fooBar', object).should.equal(true); - object.should.have.property('foo'); - object.should.have.property('bar'); - object.should.not.have.property('baz'); - }); - - - it('should remove properties that would error when `additionalProperties = false`', function() { - var ajv = new Ajv({ removeAdditional: true }); - - ajv.addSchema({ - id: '//test/fooBar', - properties: { foo: { type: 'string' }, bar: { type: 'string' } }, - additionalProperties: false - }); - - var object = { - foo: 'foo', bar: 'bar', baz: 'baz-to-be-removed' - }; - - ajv.validate('//test/fooBar', object).should.equal(true); - object.should.have.property('foo'); - object.should.have.property('bar'); - object.should.not.have.property('baz'); - }); - - - it('should remove properties that would error when `additionalProperties` is a schema', function() { - var ajv = new Ajv({ removeAdditional: 'failing' }); - - ajv.addSchema({ - id: '//test/fooBar', - properties: { foo: { type: 'string' }, bar: { type: 'string' } }, - additionalProperties: { type: 'string' } - }); - - var object = { - foo: 'foo', bar: 'bar', baz: 'baz-to-be-kept', fizz: 1000 - }; - - ajv.validate('//test/fooBar', object).should.equal(true); - object.should.have.property('foo'); - object.should.have.property('bar'); - object.should.have.property('baz'); - object.should.not.have.property('fizz'); - - ajv.addSchema({ - id: '//test/fooBar2', - properties: { foo: { type: 'string' }, bar: { type: 'string' } }, - additionalProperties: { type: 'string', pattern: '^to-be-', maxLength: 10 } - }); - - object = { - foo: 'foo', bar: 'bar', baz: 'to-be-kept', quux: 'to-be-removed', fizz: 1000 - }; - - ajv.validate('//test/fooBar2', object).should.equal(true); - object.should.have.property('foo'); - object.should.have.property('bar'); - object.should.have.property('baz'); - object.should.not.have.property('fizz'); - }); - }); - - - describe('ownProperties', function() { - var ajv, ajvOP, ajvOP1; - - beforeEach(function() { - ajv = new Ajv({ allErrors: true }); - ajvOP = new Ajv({ ownProperties: true, allErrors: true }); - ajvOP1 = new Ajv({ ownProperties: true }); - }); - - it('should only validate own properties with additionalProperties', function() { - var schema = { - properties: { a: { type: 'number' } }, - additionalProperties: false - }; - - var obj = { a: 1 }; - var proto = { b: 2 }; - test(schema, obj, proto); - }); - - it('should only validate own properties with properties keyword', function() { - var schema = { - properties: { - a: { type: 'number' }, - b: { type: 'number' } - } - }; - - var obj = { a: 1 }; - var proto = { b: 'not a number' }; - test(schema, obj, proto); - }); - - it('should only validate own properties with required keyword', function() { - var schema = { - required: ['a', 'b'] - }; - - var obj = { a: 1 }; - var proto = { b: 2 }; - test(schema, obj, proto, 1, true); - }); - - it('should only validate own properties with required keyword - many properties', function() { - ajv = new Ajv({ allErrors: true, loopRequired: 1 }); - ajvOP = new Ajv({ ownProperties: true, allErrors: true, loopRequired: 1 }); - ajvOP1 = new Ajv({ ownProperties: true, loopRequired: 1 }); - - var schema = { - required: ['a', 'b', 'c', 'd'] - }; - - var obj = { a: 1, b: 2 }; - var proto = { c: 3, d: 4 }; - test(schema, obj, proto, 2, true); - }); - - it('should only validate own properties with required keyword as $data', function() { - ajv = new Ajv({ allErrors: true, $data: true }); - ajvOP = new Ajv({ ownProperties: true, allErrors: true, $data: true }); - ajvOP1 = new Ajv({ ownProperties: true, $data: true }); - - var schema = { - required: { $data: '0/req' }, - properties: { - req: { - type: 'array', - items: { type: 'string' } - } - } - }; - - var obj = { - req: ['a', 'b'], - a: 1 - }; - var proto = { b: 2 }; - test(schema, obj, proto, 1, true); - }); - - it('should only validate own properties with properties and required keyword', function() { - var schema = { - properties: { - a: { type: 'number' }, - b: { type: 'number' } - }, - required: ['a', 'b'] - }; - - var obj = { a: 1 }; - var proto = { b: 2 }; - test(schema, obj, proto, 1, true); - }); - - it('should only validate own properties with dependencies keyword', function() { - var schema = { - dependencies: { - a: ['c'], - b: ['d'] - } - }; - - var obj = { a: 1, c: 3 }; - var proto = { b: 2 }; - test(schema, obj, proto); - - obj = { a: 1, b: 2, c: 3 }; - proto = { d: 4 }; - test(schema, obj, proto, 1, true); - }); - - it('should only validate own properties with schema dependencies', function() { - var schema = { - dependencies: { - a: { not: { required: ['c'] } }, - b: { not: { required: ['d'] } } - } - }; - - var obj = { a: 1, d: 3 }; - var proto = { b: 2 }; - test(schema, obj, proto); - - obj = { a: 1, b: 2 }; - proto = { d: 4 }; - test(schema, obj, proto); - }); - - it('should only validate own properties with patternProperties', function() { - var schema = { - patternProperties: { 'f.*o': { type: 'integer' } }, - }; - - var obj = { fooo: 1 }; - var proto = { foo: 'not a number' }; - test(schema, obj, proto); - }); - - it('should only validate own properties with patternGroups', function() { - ajv = new Ajv({ allErrors: true, patternGroups: true }); - ajvOP = new Ajv({ ownProperties: true, allErrors: true, patternGroups: true }); - - var schema = { - patternGroups: { - 'f.*o': { schema: { type: 'integer' } } - } - }; - - var obj = { fooo: 1 }; - var proto = { foo: 'not a number' }; - test(schema, obj, proto); - }); - - it('should only validate own properties with propertyNames', function() { - var schema = { - propertyNames: { - format: 'email' - } - }; - - var obj = { 'e@example.com': 2 }; - var proto = { 'not email': 1 }; - test(schema, obj, proto, 2); - }); - - function test(schema, obj, proto, errors, reverse) { - errors = errors || 1; - var validate = ajv.compile(schema); - var validateOP = ajvOP.compile(schema); - var validateOP1 = ajvOP1.compile(schema); - var data = Object.create(proto); - for (var key in obj) data[key] = obj[key]; - - if (reverse) { - validate(data) .should.equal(true); - validateOP(data) .should.equal(false); - validateOP.errors .should.have.length(errors); - validateOP1(data) .should.equal(false); - validateOP1.errors .should.have.length(1); - } else { - validate(data) .should.equal(false); - validate.errors .should.have.length(errors); - validateOP(data) .should.equal(true); - validateOP1(data) .should.equal(true); - } - } - }); - - describe('meta and validateSchema', function() { - it('should add draft-6 meta schema by default', function() { - testOptionMeta(new Ajv); - testOptionMeta(new Ajv({ meta: true })); - - function testOptionMeta(ajv) { - ajv.getSchema('http://json-schema.org/draft-06/schema') .should.be.a('function'); - ajv.validateSchema({ type: 'integer' }) .should.equal(true); - ajv.validateSchema({ type: 123 }) .should.equal(false); - should.not.throw(function() { ajv.addSchema({ type: 'integer' }); }); - should.throw(function() { ajv.addSchema({ type: 123 }); }); - } - }); - - it('should throw if meta: false and validateSchema: true', function() { - var ajv = new Ajv({ meta: false }); - should.not.exist(ajv.getSchema('http://json-schema.org/draft-06/schema')); - should.not.throw(function() { ajv.addSchema({ type: 'wrong_type' }, 'integer'); }); - }); - - it('should skip schema validation with validateSchema: false', function() { - var ajv = new Ajv; - should.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); - - ajv = new Ajv({ validateSchema: false }); - should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); - - ajv = new Ajv({ validateSchema: false, meta: false }); - should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); - }); - - it('should not throw on invalid schema with validateSchema: "log"', function() { - var logError = console.error; - var loggedError = false; - console.error = function() { loggedError = true; logError.apply(console, arguments); }; - - var ajv = new Ajv({ validateSchema: 'log' }); - should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); - loggedError .should.equal(true); - - loggedError = false; - ajv = new Ajv({ validateSchema: 'log', meta: false }); - should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); - loggedError .should.equal(false); - console.error = logError; - }); - - it('should validate v6 schema', function() { - var ajv = new Ajv; - ajv.validateSchema({ contains: { minimum: 2 } }) .should.equal(true); - ajv.validateSchema({ contains: 2 }). should.equal(false); - }); - - it('should use option meta as default meta schema', function() { - var meta = { - $schema: 'http://json-schema.org/draft-06/schema', - properties: { - myKeyword: { type: 'boolean' } - } - }; - var ajv = new Ajv({ meta: meta }); - ajv.validateSchema({ myKeyword: true }) .should.equal(true); - ajv.validateSchema({ myKeyword: 2 }) .should.equal(false); - ajv.validateSchema({ - $schema: 'http://json-schema.org/draft-06/schema', - myKeyword: 2 - }) .should.equal(true); - - ajv = new Ajv; - ajv.validateSchema({ myKeyword: true }) .should.equal(true); - ajv.validateSchema({ myKeyword: 2 }) .should.equal(true); - }); - }); - - - describe('schemas', function() { - it('should add schemas from object', function() { - var ajv = new Ajv({ schemas: { - int: { type: 'integer' }, - str: { type: 'string' } - }}); - - ajv.validate('int', 123) .should.equal(true); - ajv.validate('int', 'foo') .should.equal(false); - ajv.validate('str', 'foo') .should.equal(true); - ajv.validate('str', 123) .should.equal(false); - }); - - it('should add schemas from array', function() { - var ajv = new Ajv({ schemas: [ - { id: 'int', type: 'integer' }, - { id: 'str', type: 'string' }, - { id: 'obj', properties: { int: { $ref: 'int' }, str: { $ref: 'str' } } } - ]}); - - ajv.validate('obj', { int: 123, str: 'foo' }) .should.equal(true); - ajv.validate('obj', { int: 'foo', str: 'bar' }) .should.equal(false); - ajv.validate('obj', { int: 123, str: 456 }) .should.equal(false); - }); - }); - - - describe('format', function() { - it('should not validate formats if option format == false', function() { - var ajv = new Ajv - , ajvFF = new Ajv({ format: false }); - - var schema = { format: 'date-time' }; - var invalideDateTime = '06/19/1963 08:30:06 PST'; - - ajv.validate(schema, invalideDateTime) .should.equal(false); - ajvFF.validate(schema, invalideDateTime) .should.equal(true); - }); - }); - - - describe('formats', function() { - it('should add formats from options', function() { - var ajv = new Ajv({ formats: { - identifier: /^[a-z_$][a-z0-9_$]*$/i - }}); - - var validate = ajv.compile({ format: 'identifier' }); - validate('Abc1') .should.equal(true); - validate('123') .should.equal(false); - validate(123) .should.equal(true); - }); - }); - - - describe('missingRefs', function() { - it('should throw if ref is missing without this option', function() { - var ajv = new Ajv; - should.throw(function() { - ajv.compile({ $ref: 'missing_reference' }); - }); - }); - - it('should not throw and pass validation with missingRef == "ignore"', function() { - testMissingRefsIgnore(new Ajv({ missingRefs: 'ignore' })); - testMissingRefsIgnore(new Ajv({ missingRefs: 'ignore', allErrors: true })); - - function testMissingRefsIgnore(ajv) { - var validate = ajv.compile({ $ref: 'missing_reference' }); - validate({}) .should.equal(true); - } - }); - - it('should not throw and fail validation with missingRef == "fail" if the ref is used', function() { - testMissingRefsFail(new Ajv({ missingRefs: 'fail' })); - testMissingRefsFail(new Ajv({ missingRefs: 'fail', verbose: true })); - testMissingRefsFail(new Ajv({ missingRefs: 'fail', allErrors: true })); - testMissingRefsFail(new Ajv({ missingRefs: 'fail', allErrors: true, verbose: true })); - - function testMissingRefsFail(ajv) { - var validate = ajv.compile({ - anyOf: [ - { type: 'number' }, - { $ref: 'missing_reference' } - ] - }); - validate(123) .should.equal(true); - validate('foo') .should.equal(false); - - validate = ajv.compile({ $ref: 'missing_reference' }); - validate({}) .should.equal(false); - } - }); - }); - - - describe('uniqueItems', function() { - it('should not validate uniqueItems with uniqueItems option == false', function() { - testUniqueItems(new Ajv({ uniqueItems: false })); - testUniqueItems(new Ajv({ uniqueItems: false, allErrors: true })); - - function testUniqueItems(ajv) { - var validate = ajv.compile({ uniqueItems: true }); - validate([1,2,3]) .should.equal(true); - validate([1,1,1]) .should.equal(true); - } - }); - }); - - - describe('unicode', function() { - it('should use String.prototype.length with unicode option == false', function() { - var ajvUnicode = new Ajv; - testUnicode(new Ajv({ unicode: false })); - testUnicode(new Ajv({ unicode: false, allErrors: true })); - - function testUnicode(ajv) { - var validateWithUnicode = ajvUnicode.compile({ minLength: 2 }); - var validate = ajv.compile({ minLength: 2 }); - - validateWithUnicode('😀') .should.equal(false); - validate('😀') .should.equal(true); - - validateWithUnicode = ajvUnicode.compile({ maxLength: 1 }); - validate = ajv.compile({ maxLength: 1 }); - - validateWithUnicode('😀') .should.equal(true); - validate('😀') .should.equal(false); - } - }); - }); - - - describe('verbose', function() { - it('should add schema, parentSchema and data to errors with verbose option == true', function() { - testVerbose(new Ajv({ verbose: true })); - testVerbose(new Ajv({ verbose: true, allErrors: true })); - - function testVerbose(ajv) { - var schema = { properties: { foo: { minimum: 5 } } }; - var validate = ajv.compile(schema); - - var data = { foo: 3 }; - validate(data) .should.equal(false); - validate.errors .should.have.length(1); - var err = validate.errors[0]; - - should.equal(err.schema, 5); - err.parentSchema .should.eql({ minimum: 5 }); - err.parentSchema .should.equal(schema.properties.foo); // by reference - should.equal(err.data, 3); - } - }); - }); - - - describe('multipleOfPrecision', function() { - it('should allow for some deviation from 0 when validating multipleOf with value < 1', function() { - test(new Ajv({ multipleOfPrecision: 7 })); - test(new Ajv({ multipleOfPrecision: 7, allErrors: true })); - - function test(ajv) { - var schema = { multipleOf: 0.01 }; - var validate = ajv.compile(schema); - - validate(4.18) .should.equal(true); - validate(4.181) .should.equal(false); - - schema = { multipleOf: 0.0000001 }; - validate = ajv.compile(schema); - - validate(53.198098) .should.equal(true); - validate(53.1980981) .should.equal(true); - validate(53.19809811) .should.equal(false); - } - }); - }); - - - describe('useDefaults', function() { - it('should replace undefined property with default value', function() { - var instances = getAjvInstances({ - allErrors: true, - loopRequired: 3 - }, { useDefaults: true }); - - instances.forEach(test); - - - function test(ajv) { - var schema = { - properties: { - foo: { type: 'string', default: 'abc' }, - bar: { type: 'number', default: 1 }, - baz: { type: 'boolean', default: false }, - nil: { type: 'null', default: null }, - obj: { type: 'object', default: {} }, - arr: { type: 'array', default: [] } - }, - required: ['foo', 'bar', 'baz', 'nil', 'obj', 'arr'], - minProperties: 6 - }; - - var validate = ajv.compile(schema); - - var data = {}; - validate(data) .should.equal(true); - data .should.eql({ foo: 'abc', bar: 1, baz: false, nil: null, obj: {}, arr:[] }); - - data = { foo: 'foo', bar: 2, obj: { test: true } }; - validate(data) .should.equal(true); - data .should.eql({ foo: 'foo', bar: 2, baz: false, nil: null, obj: { test: true }, arr:[] }); - } - }); - - it('should replace undefined item with default value', function() { - test(new Ajv({ useDefaults: true })); - test(new Ajv({ useDefaults: true, allErrors: true })); - - function test(ajv) { - var schema = { - items: [ - { type: 'string', default: 'abc' }, - { type: 'number', default: 1 }, - { type: 'boolean', default: false } - ], - minItems: 3 - }; - - var validate = ajv.compile(schema); - - var data = []; - validate(data) .should.equal(true); - data .should.eql([ 'abc', 1, false ]); - - data = [ 'foo' ]; - validate(data) .should.equal(true); - data .should.eql([ 'foo', 1, false ]); - - data = ['foo', 2,'false']; - validate(data) .should.equal(false); - validate.errors .should.have.length(1); - data .should.eql([ 'foo', 2, 'false' ]); - } - }); - - - describe('useDefaults: by value / by reference', function() { - describe('using by value', function() { - it('should NOT modify underlying defaults when modifying validated data', function() { - test('value', new Ajv({ useDefaults: true })); - test('value', new Ajv({ useDefaults: true, allErrors: true })); - }); - }); - - describe('using by reference', function() { - it('should modify underlying defaults when modifying validated data', function() { - test('reference', new Ajv({ useDefaults: 'shared' })); - test('reference', new Ajv({ useDefaults: 'shared', allErrors: true })); - }); - }); - - function test(useDefaultsMode, ajv) { - var schema = { - properties: { - items: { - type: 'array', - default: ['a-default'] - } - } - }; - - var validate = ajv.compile(schema); - - var data = {}; - validate(data) .should.equal(true); - data.items .should.eql([ 'a-default' ]); - - data.items.push('another-value'); - data.items .should.eql([ 'a-default', 'another-value' ]); - - var data2 = {}; - validate(data2) .should.equal(true); - - if (useDefaultsMode == 'reference') - data2.items .should.eql([ 'a-default', 'another-value' ]); - else if (useDefaultsMode == 'value') - data2.items .should.eql([ 'a-default' ]); - else - throw new Error('unknown useDefaults mode'); - } - }); - }); - - - describe('addUsedSchema', function() { - [true, undefined].forEach(function (optionValue) { - describe('= ' + optionValue, function() { - var ajv; - - beforeEach(function() { - ajv = new Ajv({ addUsedSchema: optionValue }); - }); - - describe('compile and validate', function() { - it('should add schema', function() { - var schema = { id: 'str', type: 'string' }; - var validate = ajv.compile(schema); - validate('abc') .should.equal(true); - validate(1) .should.equal(false); - ajv.getSchema('str') .should.equal(validate); - - schema = { id: 'int', type: 'integer' }; - ajv.validate(schema, 1) .should.equal(true); - ajv.validate(schema, 'abc') .should.equal(false); - ajv.getSchema('int') .should.be.a('function'); - }); - - it('should throw with duplicate ID', function() { - ajv.compile({ id: 'str', type: 'string' }); - should.throw(function() { - ajv.compile({ id: 'str', minLength: 2 }); - }); - - var schema = { id: 'int', type: 'integer' }; - var schema2 = { id: 'int', minimum: 0 }; - ajv.validate(schema, 1) .should.equal(true); - should.throw(function() { - ajv.validate(schema2, 1); - }); - }); - }); - }); - }); - - describe('= false', function() { - var ajv; - - beforeEach(function() { - ajv = new Ajv({ addUsedSchema: false }); - }); - - - describe('compile and validate', function() { - it('should NOT add schema', function() { - var schema = { id: 'str', type: 'string' }; - var validate = ajv.compile(schema); - validate('abc') .should.equal(true); - validate(1) .should.equal(false); - should.equal(ajv.getSchema('str'), undefined); - - schema = { id: 'int', type: 'integer' }; - ajv.validate(schema, 1) .should.equal(true); - ajv.validate(schema, 'abc') .should.equal(false); - should.equal(ajv.getSchema('int'), undefined); - }); - - it('should NOT throw with duplicate ID', function() { - ajv.compile({ id: 'str', type: 'string' }); - should.not.throw(function() { - ajv.compile({ id: 'str', minLength: 2 }); - }); - - var schema = { id: 'int', type: 'integer' }; - var schema2 = { id: 'int', minimum: 0 }; - ajv.validate(schema, 1) .should.equal(true); - should.not.throw(function() { - ajv.validate(schema2, 1) .should.equal(true); - }); - }); - }); - }); - }); - - - describe('passContext', function() { - var ajv, contexts; - - beforeEach(function() { - contexts = []; - }); - - describe('= true', function() { - it('should pass this value as context to custom keyword validation function', function() { - var validate = getValidate(true); - var self = {}; - validate.call(self, {}); - contexts .should.have.length(4); - contexts.forEach(function(ctx) { - ctx .should.equal(self); - }); - }); - }); - - describe('= false', function() { - it('should pass ajv instance as context to custom keyword validation function', function() { - var validate = getValidate(false); - var self = {}; - validate.call(self, {}); - contexts .should.have.length(4); - contexts.forEach(function(ctx) { - ctx .should.equal(ajv); - }); - }); - }); - - function getValidate(passContext) { - ajv = new Ajv({ passContext: passContext, inlineRefs: false }); - ajv.addKeyword('testValidate', { validate: storeContext }); - ajv.addKeyword('testCompile', { compile: compileTestValidate }); - - var schema = { - definitions: { - test1: { - testValidate: true, - testCompile: true, - }, - test2: { - allOf: [ { $ref: '#/definitions/test1' } ] - } - }, - allOf: [ - { $ref: '#/definitions/test1' }, - { $ref: '#/definitions/test2' } - ] - }; - - return ajv.compile(schema); - } - - function storeContext() { - contexts.push(this); - return true; - } - - function compileTestValidate() { - return storeContext; - } - }); - - - describe('allErrors', function() { - it('should be disabled inside "not" keyword', function() { - test(new Ajv, false); - test(new Ajv({ allErrors: true }), true); - - function test(ajv, allErrors) { - var format1called = false - , format2called = false; - - ajv.addFormat('format1', function() { - format1called = true; - return false; - }); - - ajv.addFormat('format2', function() { - format2called = true; - return false; - }); - - var schema1 = { - allOf: [ - { format: 'format1' }, - { format: 'format2' } - ] - }; - - ajv.validate(schema1, 'abc') .should.equal(false); - ajv.errors .should.have.length(allErrors ? 2 : 1); - format1called .should.equal(true); - format2called .should.equal(allErrors); - - var schema2 = { - not: schema1 - }; - - format1called = format2called = false; - ajv.validate(schema2, 'abc') .should.equal(true); - should.equal(ajv.errors, null); - format1called .should.equal(true); - format2called .should.equal(false); - } - }); - }); - - - describe('extendRefs', function() { - describe('= true', function() { - it('should allow extending $ref with other keywords', function() { - test(new Ajv({ extendRefs: true }), true); - }); - - it('should NOT log warning if extendRefs is true', function() { - testWarning(new Ajv({ extendRefs: true })); - }); - }); - - describe('= "ignore" and default', function() { - it('should ignore other keywords when $ref is used', function() { - test(new Ajv); - test(new Ajv({ extendRefs: 'ignore' }), false); - }); - - it('should log warning when other keywords are used with $ref', function() { - testWarning(new Ajv, /keywords\signored/); - testWarning(new Ajv({ extendRefs: 'ignore' }), /keywords\signored/); - }); - }); - - describe('= "fail"', function() { - it('should fail schema compilation if other keywords are used with $ref', function() { - testFail(new Ajv({ extendRefs: 'fail' })); - - function testFail(ajv) { - should.throw(function() { - var schema = { - "definitions": { - "int": { "type": "integer" } - }, - "$ref": "#/definitions/int", - "minimum": 10 - }; - ajv.compile(schema); - }); - - should.not.throw(function() { - var schema = { - "definitions": { - "int": { "type": "integer" } - }, - "allOf": [ - { "$ref": "#/definitions/int" }, - { "minimum": 10 } - ] - }; - ajv.compile(schema); - }); - } - }); - }); - - function test(ajv, shouldExtendRef) { - var schema = { - "definitions": { - "int": { "type": "integer" } - }, - "$ref": "#/definitions/int", - "minimum": 10 - }; - - var validate = ajv.compile(schema); - validate(10) .should.equal(true); - validate(1) .should.equal(!shouldExtendRef); - - schema = { - "definitions": { - "int": { "type": "integer" } - }, - "type": "object", - "properties": { - "foo": { - "$ref": "#/definitions/int", - "minimum": 10 - }, - "bar": { - "allOf": [ - { "$ref": "#/definitions/int" }, - { "minimum": 10 } - ] - } - } - }; - - validate = ajv.compile(schema); - validate({ foo: 10, bar: 10 }) .should.equal(true); - validate({ foo: 1, bar: 10 }) .should.equal(!shouldExtendRef); - validate({ foo: 10, bar: 1 }) .should.equal(false); - } - - function testWarning(ajv, msgPattern) { - var oldConsole; - try { - oldConsole = console.warn; - var consoleMsg; - console.warn = function() { - consoleMsg = Array.prototype.join.call(arguments, ' '); - }; - - var schema = { - "definitions": { - "int": { "type": "integer" } - }, - "$ref": "#/definitions/int", - "minimum": 10 - }; - - ajv.compile(schema); - if (msgPattern) consoleMsg .should.match(msgPattern); - else should.not.exist(consoleMsg); - } finally { - console.warn = oldConsole; - } - } - }); - - - describe('sourceCode', function() { - describe('= true', function() { - it('should add source.code property', function() { - test(new Ajv({sourceCode: true})); - - function test(ajv) { - var validate = ajv.compile({ "type": "number" }); - validate.source.code .should.be.a('string'); - } - }); - }); - - describe('= false and default', function() { - it('should not add source and sourceCode properties', function() { - test(new Ajv); - test(new Ajv({sourceCode: false})); - - function test(ajv) { - var validate = ajv.compile({ "type": "number" }); - should.not.exist(validate.source); - should.not.exist(validate.sourceCode); - } - }); - }); - }); - - - describe('unknownFormats', function() { - describe('= true (default)', function() { - it('should fail schema compilation if unknown format is used', function() { - test(new Ajv); - test(new Ajv({unknownFormats: true})); - - function test(ajv) { - should.throw(function() { - ajv.compile({ format: 'unknown' }); - }); - } - }); - - it('should fail validation if unknown format is used via $data', function() { - test(new Ajv({$data: true})); - test(new Ajv({$data: true, unknownFormats: true})); - - function test(ajv) { - var validate = ajv.compile({ - properties: { - foo: { format: { $data: '1/bar' } }, - bar: { type: 'string' } - } - }); - - validate({foo: 1, bar: 'unknown'}) .should.equal(false); - validate({foo: '2016-10-16', bar: 'date'}) .should.equal(true); - validate({foo: '20161016', bar: 'date'}) .should.equal(false); - validate({foo: '20161016'}) .should.equal(true); - - validate({foo: '2016-10-16', bar: 'unknown'}) .should.equal(false); - } - }); - }); - - describe('= "ignore (default before 5.0.0)"', function() { - it('should pass schema compilation and be valid if unknown format is used', function() { - test(new Ajv({unknownFormats: 'ignore'})); - - function test(ajv) { - var validate = ajv.compile({ format: 'unknown' }); - validate('anything') .should.equal(true); - } - }); - - it('should be valid if unknown format is used via $data', function() { - test(new Ajv({$data: true, unknownFormats: 'ignore'})); - - function test(ajv) { - var validate = ajv.compile({ - properties: { - foo: { format: { $data: '1/bar' } }, - bar: { type: 'string' } - } - }); - - validate({foo: 1, bar: 'unknown'}) .should.equal(true); - validate({foo: '2016-10-16', bar: 'date'}) .should.equal(true); - validate({foo: '20161016', bar: 'date'}) .should.equal(false); - validate({foo: '20161016'}) .should.equal(true); - validate({foo: '2016-10-16', bar: 'unknown'}) .should.equal(true); - } - }); - }); - - describe('= [String]', function() { - it('should pass schema compilation and be valid if whitelisted unknown format is used', function() { - test(new Ajv({unknownFormats: ['allowed']})); - - function test(ajv) { - var validate = ajv.compile({ format: 'allowed' }); - validate('anything') .should.equal(true); - - should.throw(function() { - ajv.compile({ format: 'unknown' }); - }); - } - }); - - it('should be valid if whitelisted unknown format is used via $data', function() { - test(new Ajv({$data: true, unknownFormats: ['allowed']})); - - function test(ajv) { - var validate = ajv.compile({ - properties: { - foo: { format: { $data: '1/bar' } }, - bar: { type: 'string' } - } - }); - - validate({foo: 1, bar: 'allowed'}) .should.equal(true); - validate({foo: 1, bar: 'unknown'}) .should.equal(false); - validate({foo: '2016-10-16', bar: 'date'}) .should.equal(true); - validate({foo: '20161016', bar: 'date'}) .should.equal(false); - validate({foo: '20161016'}) .should.equal(true); - - validate({foo: '2016-10-16', bar: 'allowed'}) .should.equal(true); - validate({foo: '2016-10-16', bar: 'unknown'}) .should.equal(false); - } - }); - }); - }); - - - describe('processCode', function() { - it('should process generated code', function() { - var ajv = new Ajv; - var validate = ajv.compile({type: 'string'}); - validate.toString().split('\n').length .should.equal(1); - - var beautify = require('js-beautify').js_beautify; - var ajvPC = new Ajv({processCode: beautify}); - validate = ajvPC.compile({type: 'string'}); - validate.toString().split('\n').length .should.be.above(1); - validate('foo') .should.equal(true); - validate(1) .should.equal(false); - }); - }); - - - describe('serialize', function() { - var serializeCalled; - - it('should use custom function to serialize schema to string', function() { - serializeCalled = undefined; - var ajv = new Ajv({ serialize: serialize }); - ajv.addSchema({ type: 'string' }); - should.equal(serializeCalled, true); - }); - - function serialize(schema) { - serializeCalled = true; - return JSON.stringify(schema); - } - }); - - - describe('patternGroups without draft-06 meta-schema', function() { - it('should use default meta-schema', function() { - var ajv = new Ajv({ - patternGroups: true, - meta: require('../lib/refs/json-schema-draft-04.json') - }); - - ajv.compile({ - patternGroups: { - '^foo': { - schema: { type: 'number' }, - minimum: 1 - } - } - }); - - should.throw(function() { - ajv.compile({ - patternGroups: { - '^foo': { - schema: { type: 'wrong_type' }, - minimum: 1 - } - } - }); - }); - }); - - it('should not use meta-schema if not available', function() { - var ajv = new Ajv({ - patternGroups: true, - meta: false - }); - - ajv.compile({ - patternGroups: { - '^foo': { - schema: { type: 'number' }, - minimum: 1 - } - } - }); - - ajv.compile({ - patternGroups: { - '^foo': { - schema: { type: 'wrong_type' }, - minimum: 1 - } - } - }); - }); - }); - - - describe('schemaId', function() { - describe('= undefined (default)', function() { - it('should throw if both id and $id are available and different', function() { - var ajv = new Ajv; - - ajv.compile({ - id: 'mySchema', - $id: 'mySchema' - }); - - should.throw(function() { - ajv.compile({ - id: 'mySchema1', - $id: 'mySchema2' - }); - }); - }); - }); - - describe('= "id"', function() { - it('should use id and ignore $id', function() { - var ajv = new Ajv({schemaId: 'id'}); - - ajv.addSchema({ id: 'mySchema1', type: 'string' }); - var validate = ajv.getSchema('mySchema1'); - validate('foo') .should.equal(true); - validate(1) .should.equal(false); - - validate = ajv.compile({ $id: 'mySchema2', type: 'string' }); - should.not.exist(ajv.getSchema('mySchema2')); - }); - }); - - describe('= "$id"', function() { - it('should use $id and ignore id', function() { - var ajv = new Ajv({schemaId: '$id'}); - - ajv.addSchema({ $id: 'mySchema1', type: 'string' }); - var validate = ajv.getSchema('mySchema1'); - validate('foo') .should.equal(true); - validate(1) .should.equal(false); - - validate = ajv.compile({ id: 'mySchema2', type: 'string' }); - should.not.exist(ajv.getSchema('mySchema2')); - }); - }); - }); -}); diff --git a/spec/options/comment.spec.js b/spec/options/comment.spec.js new file mode 100644 index 000000000..efdc98b5e --- /dev/null +++ b/spec/options/comment.spec.js @@ -0,0 +1,92 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('$comment option', function() { + describe('= true', function() { + var logCalls, consoleLog; + + beforeEach(function () { + consoleLog = console.log; + console.log = log; + }); + + afterEach(function () { + console.log = consoleLog; + }); + + function log() { + logCalls.push(Array.prototype.slice.call(arguments)); + } + + it('should log the text from $comment keyword', function() { + var schema = { + properties: { + foo: {$comment: 'property foo'}, + bar: {$comment: 'property bar', type: 'integer'} + } + }; + + var ajv = new Ajv({$comment: true}); + var fullAjv = new Ajv({allErrors: true, $comment: true}); + + [ajv, fullAjv].forEach(function (_ajv) { + var validate = _ajv.compile(schema); + + test({}, true, []); + test({foo: 1}, true, [['property foo']]); + test({foo: 1, bar: 2}, true, [['property foo'], ['property bar']]); + test({foo: 1, bar: 'baz'}, false, [['property foo'], ['property bar']]); + + function test(data, valid, expectedLogCalls) { + logCalls = []; + validate(data) .should.equal(valid); + logCalls .should.eql(expectedLogCalls); + } + }); + + console.log = consoleLog; + }); + }); + + describe('function hook', function() { + var hookCalls; + + function hook() { + hookCalls.push(Array.prototype.slice.call(arguments)); + } + + it('should pass the text from $comment keyword to the hook', function() { + var schema = { + properties: { + foo: {$comment: 'property foo'}, + bar: {$comment: 'property bar', type: 'integer'} + } + }; + + var ajv = new Ajv({$comment: hook}); + var fullAjv = new Ajv({allErrors: true, $comment: hook}); + + [ajv, fullAjv].forEach(function (_ajv) { + var validate = _ajv.compile(schema); + + test({}, true, []); + test({foo: 1}, true, [['property foo', '#/properties/foo/$comment', schema]]); + test({foo: 1, bar: 2}, true, + [['property foo', '#/properties/foo/$comment', schema], + ['property bar', '#/properties/bar/$comment', schema]]); + test({foo: 1, bar: 'baz'}, false, + [['property foo', '#/properties/foo/$comment', schema], + ['property bar', '#/properties/bar/$comment', schema]]); + + function test(data, valid, expectedHookCalls) { + hookCalls = []; + validate(data) .should.equal(valid); + hookCalls .should.eql(expectedHookCalls); + } + }); + }); + }); +}); diff --git a/spec/options/meta_validateSchema.spec.js b/spec/options/meta_validateSchema.spec.js new file mode 100644 index 000000000..2e287da75 --- /dev/null +++ b/spec/options/meta_validateSchema.spec.js @@ -0,0 +1,79 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('meta and validateSchema options', function() { + it('should add draft-7 meta schema by default', function() { + testOptionMeta(new Ajv); + testOptionMeta(new Ajv({ meta: true })); + + function testOptionMeta(ajv) { + ajv.getSchema('http://json-schema.org/draft-07/schema') .should.be.a('function'); + ajv.validateSchema({ type: 'integer' }) .should.equal(true); + ajv.validateSchema({ type: 123 }) .should.equal(false); + should.not.throw(function() { ajv.addSchema({ type: 'integer' }); }); + should.throw(function() { ajv.addSchema({ type: 123 }); }); + } + }); + + it('should throw if meta: false and validateSchema: true', function() { + var ajv = new Ajv({ meta: false }); + should.not.exist(ajv.getSchema('http://json-schema.org/draft-07/schema')); + should.not.throw(function() { ajv.addSchema({ type: 'wrong_type' }, 'integer'); }); + }); + + it('should skip schema validation with validateSchema: false', function() { + var ajv = new Ajv; + should.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); + + ajv = new Ajv({ validateSchema: false }); + should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); + + ajv = new Ajv({ validateSchema: false, meta: false }); + should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); + }); + + it('should not throw on invalid schema with validateSchema: "log"', function() { + var logError = console.error; + var loggedError = false; + console.error = function() { loggedError = true; logError.apply(console, arguments); }; + + var ajv = new Ajv({ validateSchema: 'log' }); + should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); + loggedError .should.equal(true); + + loggedError = false; + ajv = new Ajv({ validateSchema: 'log', meta: false }); + should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); + loggedError .should.equal(false); + console.error = logError; + }); + + it('should validate v6 schema', function() { + var ajv = new Ajv; + ajv.validateSchema({ contains: { minimum: 2 } }) .should.equal(true); + ajv.validateSchema({ contains: 2 }). should.equal(false); + }); + + it('should use option meta as default meta schema', function() { + var meta = { + $schema: 'http://json-schema.org/draft-07/schema', + properties: { + myKeyword: { type: 'boolean' } + } + }; + var ajv = new Ajv({ meta: meta }); + ajv.validateSchema({ myKeyword: true }) .should.equal(true); + ajv.validateSchema({ myKeyword: 2 }) .should.equal(false); + ajv.validateSchema({ + $schema: 'http://json-schema.org/draft-07/schema', + myKeyword: 2 + }) .should.equal(true); + + ajv = new Ajv; + ajv.validateSchema({ myKeyword: true }) .should.equal(true); + ajv.validateSchema({ myKeyword: 2 }) .should.equal(true); + }); +}); diff --git a/spec/options/nullable.spec.js b/spec/options/nullable.spec.js new file mode 100644 index 000000000..df1bda5e4 --- /dev/null +++ b/spec/options/nullable.spec.js @@ -0,0 +1,97 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('nullable option', function() { + var ajv; + + describe('= true', function() { + beforeEach(function () { + ajv = new Ajv({ + nullable: true + }); + }); + + it('should add keyword "nullable"', function() { + testNullable({ + type: 'number', + nullable: true + }); + + testNullable({ + type: ['number'], + nullable: true + }); + + testNullable({ + type: ['number', 'null'] + }); + + testNullable({ + type: ['number', 'null'], + nullable: true + }); + + testNotNullable({type: 'number'}); + + testNotNullable({type: ['number']}); + }); + + it('should respect "nullable" == false with opts.nullable == true', function() { + testNotNullable({ + type: 'number', + nullable: false + }); + + testNotNullable({ + type: ['number'], + nullable: false + }); + }); + }); + + describe('without option "nullable"', function() { + it('should ignore keyword nullable', function() { + ajv = new Ajv; + + testNotNullable({ + type: 'number', + nullable: true + }); + + testNotNullable({ + type: ['number'], + nullable: true + }); + + testNullable({ + type: ['number', 'null'], + }); + + testNullable({ + type: ['number', 'null'], + nullable: true + }); + + should.not.throw(function () { + ajv.compile({nullable: false}); + }); + }); + }); + + function testNullable(schema) { + var validate = ajv.compile(schema); + validate(1) .should.equal(true); + validate(null) .should.equal(true); + validate('1') .should.equal(false); + } + + function testNotNullable(schema) { + var validate = ajv.compile(schema); + validate(1) .should.equal(true); + validate(null) .should.equal(false); + validate('1') .should.equal(false); + } +}); diff --git a/spec/options/options_add_schemas.spec.js b/spec/options/options_add_schemas.spec.js new file mode 100644 index 000000000..e1a59f236 --- /dev/null +++ b/spec/options/options_add_schemas.spec.js @@ -0,0 +1,130 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('options to add schemas', function() { + describe('schemas', function() { + it('should add schemas from object', function() { + var ajv = new Ajv({ schemas: { + int: { type: 'integer' }, + str: { type: 'string' } + }}); + + ajv.validate('int', 123) .should.equal(true); + ajv.validate('int', 'foo') .should.equal(false); + ajv.validate('str', 'foo') .should.equal(true); + ajv.validate('str', 123) .should.equal(false); + }); + + it('should add schemas from array', function() { + var ajv = new Ajv({ schemas: [ + { $id: 'int', type: 'integer' }, + { $id: 'str', type: 'string' }, + { $id: 'obj', properties: { int: { $ref: 'int' }, str: { $ref: 'str' } } } + ]}); + + ajv.validate('obj', { int: 123, str: 'foo' }) .should.equal(true); + ajv.validate('obj', { int: 'foo', str: 'bar' }) .should.equal(false); + ajv.validate('obj', { int: 123, str: 456 }) .should.equal(false); + }); + }); + + + describe('addUsedSchema', function() { + [true, undefined].forEach(function (optionValue) { + describe('= ' + optionValue, function() { + var ajv; + + beforeEach(function() { + ajv = new Ajv({ addUsedSchema: optionValue }); + }); + + describe('compile and validate', function() { + it('should add schema', function() { + var schema = { $id: 'str', type: 'string' }; + var validate = ajv.compile(schema); + validate('abc') .should.equal(true); + validate(1) .should.equal(false); + ajv.getSchema('str') .should.equal(validate); + + schema = { $id: 'int', type: 'integer' }; + ajv.validate(schema, 1) .should.equal(true); + ajv.validate(schema, 'abc') .should.equal(false); + ajv.getSchema('int') .should.be.a('function'); + }); + + it('should throw with duplicate ID', function() { + ajv.compile({ $id: 'str', type: 'string' }); + should.throw(function() { + ajv.compile({ $id: 'str', minLength: 2 }); + }); + + var schema = { $id: 'int', type: 'integer' }; + var schema2 = { $id: 'int', minimum: 0 }; + ajv.validate(schema, 1) .should.equal(true); + should.throw(function() { + ajv.validate(schema2, 1); + }); + }); + }); + }); + }); + + describe('= false', function() { + var ajv; + + beforeEach(function() { + ajv = new Ajv({ addUsedSchema: false }); + }); + + + describe('compile and validate', function() { + it('should NOT add schema', function() { + var schema = { $id: 'str', type: 'string' }; + var validate = ajv.compile(schema); + validate('abc') .should.equal(true); + validate(1) .should.equal(false); + should.equal(ajv.getSchema('str'), undefined); + + schema = { $id: 'int', type: 'integer' }; + ajv.validate(schema, 1) .should.equal(true); + ajv.validate(schema, 'abc') .should.equal(false); + should.equal(ajv.getSchema('int'), undefined); + }); + + it('should NOT throw with duplicate ID', function() { + ajv.compile({ $id: 'str', type: 'string' }); + should.not.throw(function() { + ajv.compile({ $id: 'str', minLength: 2 }); + }); + + var schema = { $id: 'int', type: 'integer' }; + var schema2 = { $id: 'int', minimum: 0 }; + ajv.validate(schema, 1) .should.equal(true); + should.not.throw(function() { + ajv.validate(schema2, 1) .should.equal(true); + }); + }); + }); + }); + }); + + + describe('serialize', function() { + var serializeCalled; + + it('should use custom function to serialize schema to string', function() { + serializeCalled = undefined; + var ajv = new Ajv({ serialize: serialize }); + ajv.addSchema({ type: 'string' }); + should.equal(serializeCalled, true); + }); + + function serialize(schema) { + serializeCalled = true; + return JSON.stringify(schema); + } + }); +}); diff --git a/spec/options/options_code.spec.js b/spec/options/options_code.spec.js new file mode 100644 index 000000000..8884c8c4c --- /dev/null +++ b/spec/options/options_code.spec.js @@ -0,0 +1,115 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('code generation options', function () { + describe('sourceCode', function() { + describe('= true', function() { + it('should add source.code property', function() { + test(new Ajv({sourceCode: true})); + + function test(ajv) { + var validate = ajv.compile({ "type": "number" }); + validate.source.code .should.be.a('string'); + } + }); + }); + + describe('= false and default', function() { + it('should not add source and sourceCode properties', function() { + test(new Ajv); + test(new Ajv({sourceCode: false})); + + function test(ajv) { + var validate = ajv.compile({ "type": "number" }); + should.not.exist(validate.source); + should.not.exist(validate.sourceCode); + } + }); + }); + }); + + + describe('processCode', function() { + it('should process generated code', function() { + var ajv = new Ajv; + var validate = ajv.compile({type: 'string'}); + validate.toString().split('\n').length .should.equal(1); + + var beautify = require('js-beautify').js_beautify; + var ajvPC = new Ajv({processCode: beautify}); + validate = ajvPC.compile({type: 'string'}); + validate.toString().split('\n').length .should.be.above(1); + validate('foo') .should.equal(true); + validate(1) .should.equal(false); + }); + }); + + + describe('passContext option', function() { + var ajv, contexts; + + beforeEach(function() { + contexts = []; + }); + + describe('= true', function() { + it('should pass this value as context to custom keyword validation function', function() { + var validate = getValidate(true); + var self = {}; + validate.call(self, {}); + contexts .should.have.length(4); + contexts.forEach(function(ctx) { + ctx .should.equal(self); + }); + }); + }); + + describe('= false', function() { + it('should pass ajv instance as context to custom keyword validation function', function() { + var validate = getValidate(false); + var self = {}; + validate.call(self, {}); + contexts .should.have.length(4); + contexts.forEach(function(ctx) { + ctx .should.equal(ajv); + }); + }); + }); + + function getValidate(passContext) { + ajv = new Ajv({ passContext: passContext, inlineRefs: false }); + ajv.addKeyword('testValidate', { validate: storeContext }); + ajv.addKeyword('testCompile', { compile: compileTestValidate }); + + var schema = { + definitions: { + test1: { + testValidate: true, + testCompile: true, + }, + test2: { + allOf: [ { $ref: '#/definitions/test1' } ] + } + }, + allOf: [ + { $ref: '#/definitions/test1' }, + { $ref: '#/definitions/test2' } + ] + }; + + return ajv.compile(schema); + } + + function storeContext() { + contexts.push(this); + return true; + } + + function compileTestValidate() { + return storeContext; + } + }); +}); diff --git a/spec/options/options_refs.spec.js b/spec/options/options_refs.spec.js new file mode 100644 index 000000000..1f1d20a75 --- /dev/null +++ b/spec/options/options_refs.spec.js @@ -0,0 +1,167 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('referenced schema options', function() { + describe('extendRefs', function() { + describe('= true', function() { + it('should allow extending $ref with other keywords', function() { + test(new Ajv({ extendRefs: true }), true); + }); + + it('should NOT log warning if extendRefs is true', function() { + testWarning(new Ajv({ extendRefs: true })); + }); + }); + + describe('= "ignore" and default', function() { + it('should ignore other keywords when $ref is used', function() { + test(new Ajv); + test(new Ajv({ extendRefs: 'ignore' }), false); + }); + + it('should log warning when other keywords are used with $ref', function() { + testWarning(new Ajv, /keywords\signored/); + testWarning(new Ajv({ extendRefs: 'ignore' }), /keywords\signored/); + }); + }); + + describe('= "fail"', function() { + it('should fail schema compilation if other keywords are used with $ref', function() { + testFail(new Ajv({ extendRefs: 'fail' })); + + function testFail(ajv) { + should.throw(function() { + var schema = { + "definitions": { + "int": { "type": "integer" } + }, + "$ref": "#/definitions/int", + "minimum": 10 + }; + ajv.compile(schema); + }); + + should.not.throw(function() { + var schema = { + "definitions": { + "int": { "type": "integer" } + }, + "allOf": [ + { "$ref": "#/definitions/int" }, + { "minimum": 10 } + ] + }; + ajv.compile(schema); + }); + } + }); + }); + + function test(ajv, shouldExtendRef) { + var schema = { + "definitions": { + "int": { "type": "integer" } + }, + "$ref": "#/definitions/int", + "minimum": 10 + }; + + var validate = ajv.compile(schema); + validate(10) .should.equal(true); + validate(1) .should.equal(!shouldExtendRef); + + schema = { + "definitions": { + "int": { "type": "integer" } + }, + "type": "object", + "properties": { + "foo": { + "$ref": "#/definitions/int", + "minimum": 10 + }, + "bar": { + "allOf": [ + { "$ref": "#/definitions/int" }, + { "minimum": 10 } + ] + } + } + }; + + validate = ajv.compile(schema); + validate({ foo: 10, bar: 10 }) .should.equal(true); + validate({ foo: 1, bar: 10 }) .should.equal(!shouldExtendRef); + validate({ foo: 10, bar: 1 }) .should.equal(false); + } + + function testWarning(ajv, msgPattern) { + var oldConsole; + try { + oldConsole = console.warn; + var consoleMsg; + console.warn = function() { + consoleMsg = Array.prototype.join.call(arguments, ' '); + }; + + var schema = { + "definitions": { + "int": { "type": "integer" } + }, + "$ref": "#/definitions/int", + "minimum": 10 + }; + + ajv.compile(schema); + if (msgPattern) consoleMsg .should.match(msgPattern); + else should.not.exist(consoleMsg); + } finally { + console.warn = oldConsole; + } + } + }); + + + describe('missingRefs', function() { + it('should throw if ref is missing without this option', function() { + var ajv = new Ajv; + should.throw(function() { + ajv.compile({ $ref: 'missing_reference' }); + }); + }); + + it('should not throw and pass validation with missingRef == "ignore"', function() { + testMissingRefsIgnore(new Ajv({ missingRefs: 'ignore' })); + testMissingRefsIgnore(new Ajv({ missingRefs: 'ignore', allErrors: true })); + + function testMissingRefsIgnore(ajv) { + var validate = ajv.compile({ $ref: 'missing_reference' }); + validate({}) .should.equal(true); + } + }); + + it('should not throw and fail validation with missingRef == "fail" if the ref is used', function() { + testMissingRefsFail(new Ajv({ missingRefs: 'fail' })); + testMissingRefsFail(new Ajv({ missingRefs: 'fail', verbose: true })); + testMissingRefsFail(new Ajv({ missingRefs: 'fail', allErrors: true })); + testMissingRefsFail(new Ajv({ missingRefs: 'fail', allErrors: true, verbose: true })); + + function testMissingRefsFail(ajv) { + var validate = ajv.compile({ + anyOf: [ + { type: 'number' }, + { $ref: 'missing_reference' } + ] + }); + validate(123) .should.equal(true); + validate('foo') .should.equal(false); + + validate = ajv.compile({ $ref: 'missing_reference' }); + validate({}) .should.equal(false); + } + }); + }); +}); diff --git a/spec/options/options_reporting.spec.js b/spec/options/options_reporting.spec.js new file mode 100644 index 000000000..578611e80 --- /dev/null +++ b/spec/options/options_reporting.spec.js @@ -0,0 +1,158 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('reporting options', function () { + describe('verbose', function() { + it('should add schema, parentSchema and data to errors with verbose option == true', function() { + testVerbose(new Ajv({ verbose: true })); + testVerbose(new Ajv({ verbose: true, allErrors: true })); + + function testVerbose(ajv) { + var schema = { properties: { foo: { minimum: 5 } } }; + var validate = ajv.compile(schema); + + var data = { foo: 3 }; + validate(data) .should.equal(false); + validate.errors .should.have.length(1); + var err = validate.errors[0]; + + should.equal(err.schema, 5); + err.parentSchema .should.eql({ minimum: 5 }); + err.parentSchema .should.equal(schema.properties.foo); // by reference + should.equal(err.data, 3); + } + }); + }); + + + describe('allErrors', function() { + it('should be disabled inside "not" keyword', function() { + test(new Ajv, false); + test(new Ajv({ allErrors: true }), true); + + function test(ajv, allErrors) { + var format1called = false + , format2called = false; + + ajv.addFormat('format1', function() { + format1called = true; + return false; + }); + + ajv.addFormat('format2', function() { + format2called = true; + return false; + }); + + var schema1 = { + allOf: [ + { format: 'format1' }, + { format: 'format2' } + ] + }; + + ajv.validate(schema1, 'abc') .should.equal(false); + ajv.errors .should.have.length(allErrors ? 2 : 1); + format1called .should.equal(true); + format2called .should.equal(allErrors); + + var schema2 = { + not: schema1 + }; + + format1called = format2called = false; + ajv.validate(schema2, 'abc') .should.equal(true); + should.equal(ajv.errors, null); + format1called .should.equal(true); + format2called .should.equal(false); + } + }); + }); + + + describe('logger', function() { + /** + * The logger option tests are based on the meta scenario which writes into the logger.warn + */ + + var origConsoleWarn = console.warn; + var consoleCalled; + + beforeEach(function() { + consoleCalled = false; + console.warn = function() { + consoleCalled = true; + }; + }); + + afterEach(function() { + console.warn = origConsoleWarn; + }); + + it('no custom logger is given - global console should be used', function() { + var ajv = new Ajv({ + meta: false + }); + + ajv.compile({ + type: 'number', + minimum: 1 + }); + + should.equal(consoleCalled, true); + }); + + it('custom logger is an object - logs should only report to it', function() { + var loggerCalled = false; + + var logger = { + warn: log, + log: log, + error: log + }; + + var ajv = new Ajv({ + meta: false, + logger: logger + }); + + ajv.compile({ + type: 'number', + minimum: 1 + }); + + should.equal(loggerCalled, true); + should.equal(consoleCalled, false); + + function log() { + loggerCalled = true; + } + }); + + it('logger option is false - no logs should be reported', function() { + var ajv = new Ajv({ + meta: false, + logger: false + }); + + ajv.compile({ + type: 'number', + minimum: 1 + }); + + should.equal(consoleCalled, false); + }); + + it('logger option is an object without required methods - an error should be thrown', function() { + (function(){ + new Ajv({ + meta: false, + logger: {} + }); + }).should.throw(Error, /logger must implement log, warn and error methods/); + }); + }); +}); diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.js new file mode 100644 index 000000000..950a0c289 --- /dev/null +++ b/spec/options/options_validation.spec.js @@ -0,0 +1,116 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('validation options', function() { + describe('format', function() { + it('should not validate formats if option format == false', function() { + var ajv = new Ajv + , ajvFF = new Ajv({ format: false }); + + var schema = { format: 'date-time' }; + var invalideDateTime = '06/19/1963 08:30:06 PST'; + + ajv.validate(schema, invalideDateTime) .should.equal(false); + ajvFF.validate(schema, invalideDateTime) .should.equal(true); + }); + }); + + + describe('formats', function() { + it('should add formats from options', function() { + var ajv = new Ajv({ formats: { + identifier: /^[a-z_$][a-z0-9_$]*$/i + }}); + + var validate = ajv.compile({ format: 'identifier' }); + + validate('Abc1') .should.equal(true); + validate('foo bar') .should.equal(false); + validate('123') .should.equal(false); + validate(123) .should.equal(true); + }); + }); + + describe('keywords', function() { + it('should add keywords from options', function() { + var ajv = new Ajv({ keywords: { + identifier: { + type: 'string', + validate: function (schema, data ) { + return /^[a-z_$][a-z0-9_$]*$/i.test(data); + } + } + }}); + + var validate = ajv.compile({ identifier: true }); + + validate('Abc1') .should.equal(true); + validate('foo bar') .should.equal(false); + validate('123') .should.equal(false); + validate(123) .should.equal(true); + }); + }); + + + describe('uniqueItems', function() { + it('should not validate uniqueItems with uniqueItems option == false', function() { + testUniqueItems(new Ajv({ uniqueItems: false })); + testUniqueItems(new Ajv({ uniqueItems: false, allErrors: true })); + + function testUniqueItems(ajv) { + var validate = ajv.compile({ uniqueItems: true }); + validate([1,2,3]) .should.equal(true); + validate([1,1,1]) .should.equal(true); + } + }); + }); + + + describe('unicode', function() { + it('should use String.prototype.length with unicode option == false', function() { + var ajvUnicode = new Ajv; + testUnicode(new Ajv({ unicode: false })); + testUnicode(new Ajv({ unicode: false, allErrors: true })); + + function testUnicode(ajv) { + var validateWithUnicode = ajvUnicode.compile({ minLength: 2 }); + var validate = ajv.compile({ minLength: 2 }); + + validateWithUnicode('😀') .should.equal(false); + validate('😀') .should.equal(true); + + validateWithUnicode = ajvUnicode.compile({ maxLength: 1 }); + validate = ajv.compile({ maxLength: 1 }); + + validateWithUnicode('😀') .should.equal(true); + validate('😀') .should.equal(false); + } + }); + }); + + + describe('multipleOfPrecision', function() { + it('should allow for some deviation from 0 when validating multipleOf with value < 1', function() { + test(new Ajv({ multipleOfPrecision: 7 })); + test(new Ajv({ multipleOfPrecision: 7, allErrors: true })); + + function test(ajv) { + var schema = { multipleOf: 0.01 }; + var validate = ajv.compile(schema); + + validate(4.18) .should.equal(true); + validate(4.181) .should.equal(false); + + schema = { multipleOf: 0.0000001 }; + validate = ajv.compile(schema); + + validate(53.198098) .should.equal(true); + validate(53.1980981) .should.equal(true); + validate(53.19809811) .should.equal(false); + } + }); + }); +}); diff --git a/spec/options/ownProperties.spec.js b/spec/options/ownProperties.spec.js new file mode 100644 index 000000000..312579f96 --- /dev/null +++ b/spec/options/ownProperties.spec.js @@ -0,0 +1,178 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('ownProperties option', function() { + var ajv, ajvOP, ajvOP1; + + beforeEach(function() { + ajv = new Ajv({ allErrors: true }); + ajvOP = new Ajv({ ownProperties: true, allErrors: true }); + ajvOP1 = new Ajv({ ownProperties: true }); + }); + + it('should only validate own properties with additionalProperties', function() { + var schema = { + properties: { a: { type: 'number' } }, + additionalProperties: false + }; + + var obj = { a: 1 }; + var proto = { b: 2 }; + test(schema, obj, proto); + }); + + it('should only validate own properties with properties keyword', function() { + var schema = { + properties: { + a: { type: 'number' }, + b: { type: 'number' } + } + }; + + var obj = { a: 1 }; + var proto = { b: 'not a number' }; + test(schema, obj, proto); + }); + + it('should only validate own properties with required keyword', function() { + var schema = { + required: ['a', 'b'] + }; + + var obj = { a: 1 }; + var proto = { b: 2 }; + test(schema, obj, proto, 1, true); + }); + + it('should only validate own properties with required keyword - many properties', function() { + ajv = new Ajv({ allErrors: true, loopRequired: 1 }); + ajvOP = new Ajv({ ownProperties: true, allErrors: true, loopRequired: 1 }); + ajvOP1 = new Ajv({ ownProperties: true, loopRequired: 1 }); + + var schema = { + required: ['a', 'b', 'c', 'd'] + }; + + var obj = { a: 1, b: 2 }; + var proto = { c: 3, d: 4 }; + test(schema, obj, proto, 2, true); + }); + + it('should only validate own properties with required keyword as $data', function() { + ajv = new Ajv({ allErrors: true, $data: true }); + ajvOP = new Ajv({ ownProperties: true, allErrors: true, $data: true }); + ajvOP1 = new Ajv({ ownProperties: true, $data: true }); + + var schema = { + required: { $data: '0/req' }, + properties: { + req: { + type: 'array', + items: { type: 'string' } + } + } + }; + + var obj = { + req: ['a', 'b'], + a: 1 + }; + var proto = { b: 2 }; + test(schema, obj, proto, 1, true); + }); + + it('should only validate own properties with properties and required keyword', function() { + var schema = { + properties: { + a: { type: 'number' }, + b: { type: 'number' } + }, + required: ['a', 'b'] + }; + + var obj = { a: 1 }; + var proto = { b: 2 }; + test(schema, obj, proto, 1, true); + }); + + it('should only validate own properties with dependencies keyword', function() { + var schema = { + dependencies: { + a: ['c'], + b: ['d'] + } + }; + + var obj = { a: 1, c: 3 }; + var proto = { b: 2 }; + test(schema, obj, proto); + + obj = { a: 1, b: 2, c: 3 }; + proto = { d: 4 }; + test(schema, obj, proto, 1, true); + }); + + it('should only validate own properties with schema dependencies', function() { + var schema = { + dependencies: { + a: { not: { required: ['c'] } }, + b: { not: { required: ['d'] } } + } + }; + + var obj = { a: 1, d: 3 }; + var proto = { b: 2 }; + test(schema, obj, proto); + + obj = { a: 1, b: 2 }; + proto = { d: 4 }; + test(schema, obj, proto); + }); + + it('should only validate own properties with patternProperties', function() { + var schema = { + patternProperties: { 'f.*o': { type: 'integer' } }, + }; + + var obj = { fooo: 1 }; + var proto = { foo: 'not a number' }; + test(schema, obj, proto); + }); + + it('should only validate own properties with propertyNames', function() { + var schema = { + propertyNames: { + format: 'email' + } + }; + + var obj = { 'e@example.com': 2 }; + var proto = { 'not email': 1 }; + test(schema, obj, proto, 2); + }); + + function test(schema, obj, proto, errors, reverse) { + errors = errors || 1; + var validate = ajv.compile(schema); + var validateOP = ajvOP.compile(schema); + var validateOP1 = ajvOP1.compile(schema); + var data = Object.create(proto); + for (var key in obj) data[key] = obj[key]; + + if (reverse) { + validate(data) .should.equal(true); + validateOP(data) .should.equal(false); + validateOP.errors .should.have.length(errors); + validateOP1(data) .should.equal(false); + validateOP1.errors .should.have.length(1); + } else { + validate(data) .should.equal(false); + validate.errors .should.have.length(errors); + validateOP(data) .should.equal(true); + validateOP1(data) .should.equal(true); + } + } +}); diff --git a/spec/options/removeAdditional.spec.js b/spec/options/removeAdditional.spec.js new file mode 100644 index 000000000..1eef0b795 --- /dev/null +++ b/spec/options/removeAdditional.spec.js @@ -0,0 +1,122 @@ +'use strict'; + +var Ajv = require('../ajv'); +require('../chai').should(); + + +describe('removeAdditional option', function() { + it('should remove all additional properties', function() { + var ajv = new Ajv({ removeAdditional: 'all' }); + + ajv.addSchema({ + $id: '//test/fooBar', + properties: { foo: { type: 'string' }, bar: { type: 'string' } } + }); + + var object = { + foo: 'foo', bar: 'bar', baz: 'baz-to-be-removed' + }; + + ajv.validate('//test/fooBar', object).should.equal(true); + object.should.have.property('foo'); + object.should.have.property('bar'); + object.should.not.have.property('baz'); + }); + + + it('should remove properties that would error when `additionalProperties = false`', function() { + var ajv = new Ajv({ removeAdditional: true }); + + ajv.addSchema({ + $id: '//test/fooBar', + properties: { foo: { type: 'string' }, bar: { type: 'string' } }, + additionalProperties: false + }); + + var object = { + foo: 'foo', bar: 'bar', baz: 'baz-to-be-removed' + }; + + ajv.validate('//test/fooBar', object).should.equal(true); + object.should.have.property('foo'); + object.should.have.property('bar'); + object.should.not.have.property('baz'); + }); + + + it('should remove properties that would error when `additionalProperties = false` (many properties, boolean schema)', function() { + var ajv = new Ajv({removeAdditional: true}); + + var schema = { + properties: { + obj: { + additionalProperties: false, + properties: { + a: { type: 'string' }, + b: false, + c: { type: 'string' }, + d: { type: 'string' }, + e: { type: 'string' }, + f: { type: 'string' }, + g: { type: 'string' }, + h: { type: 'string' }, + i: { type: 'string' } + } + } + } + }; + + var data = { + obj: { + a: 'valid', + b: 'should not be removed', + additional: 'will be removed' + } + }; + + ajv.validate(schema, data) .should.equal(false); + data .should.eql({ + obj: { + a: 'valid', + b: 'should not be removed' + } + }); + }); + + + it('should remove properties that would error when `additionalProperties` is a schema', function() { + var ajv = new Ajv({ removeAdditional: 'failing' }); + + ajv.addSchema({ + $id: '//test/fooBar', + properties: { foo: { type: 'string' }, bar: { type: 'string' } }, + additionalProperties: { type: 'string' } + }); + + var object = { + foo: 'foo', bar: 'bar', baz: 'baz-to-be-kept', fizz: 1000 + }; + + ajv.validate('//test/fooBar', object).should.equal(true); + object.should.have.property('foo'); + object.should.have.property('bar'); + object.should.have.property('baz'); + object.should.not.have.property('fizz'); + + ajv.addSchema({ + $id: '//test/fooBar2', + properties: { foo: { type: 'string' }, bar: { type: 'string' } }, + additionalProperties: { type: 'string', pattern: '^to-be-', maxLength: 10 } + }); + + object = { + foo: 'foo', bar: 'bar', baz: 'to-be-kept', quux: 'to-be-removed', fizz: 1000 + }; + + ajv.validate('//test/fooBar2', object).should.equal(true); + object.should.have.property('foo'); + object.should.have.property('bar'); + object.should.have.property('baz'); + object.should.not.have.property('fizz'); + }); +}); diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.js new file mode 100644 index 000000000..f8871034f --- /dev/null +++ b/spec/options/schemaId.spec.js @@ -0,0 +1,72 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('schemaId option', function() { + describe('= "$id" (default)', function() { + it('should use $id and ignore id', function() { + test(new Ajv); + test(new Ajv({schemaId: '$id'})); + + function test(ajv) { + ajv.addSchema({ $id: 'mySchema1', type: 'string' }); + var validate = ajv.getSchema('mySchema1'); + validate('foo') .should.equal(true); + validate(1) .should.equal(false); + + validate = ajv.compile({ id: 'mySchema2', type: 'string' }); + should.not.exist(ajv.getSchema('mySchema2')); + } + }); + }); + + describe('= "id"', function() { + it('should use id and ignore $id', function() { + var ajv = new Ajv({schemaId: 'id', meta: false}); + ajv.addMetaSchema(require('../../lib/refs/json-schema-draft-04.json')); + ajv._opts.defaultMeta = 'http://json-schema.org/draft-04/schema#'; + + ajv.addSchema({ id: 'mySchema1', type: 'string' }); + var validate = ajv.getSchema('mySchema1'); + validate('foo') .should.equal(true); + validate(1) .should.equal(false); + + validate = ajv.compile({ $id: 'mySchema2', type: 'string' }); + should.not.exist(ajv.getSchema('mySchema2')); + }); + }); + + describe('= "auto"', function() { + it('should use both id and $id', function() { + var ajv = new Ajv({schemaId: 'auto'}); + + ajv.addSchema({ $id: 'mySchema1', type: 'string' }); + var validate = ajv.getSchema('mySchema1'); + validate('foo') .should.equal(true); + validate(1) .should.equal(false); + + ajv.addSchema({ id: 'mySchema2', type: 'string' }); + validate = ajv.getSchema('mySchema2'); + validate('foo') .should.equal(true); + validate(1) .should.equal(false); + }); + + it('should throw if both id and $id are available and different', function() { + var ajv = new Ajv({schemaId: 'auto'}); + + ajv.compile({ + id: 'mySchema', + $id: 'mySchema' + }); + + should.throw(function() { + ajv.compile({ + id: 'mySchema1', + $id: 'mySchema2' + }); + }); + }); + }); +}); diff --git a/spec/options/strictDefaults.spec.js b/spec/options/strictDefaults.spec.js new file mode 100644 index 000000000..1f1093a69 --- /dev/null +++ b/spec/options/strictDefaults.spec.js @@ -0,0 +1,165 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('strictDefaults option', function() { + describe('useDefaults = true', function() { + describe('strictDefaults = false', function() { + it('should NOT throw an error or log a warning given an ignored default', function() { + var output = {}; + var ajv = new Ajv({ + useDefaults: true, + strictDefaults: false, + logger: getLogger(output) + }); + var schema = { + default: 5, + properties: {} + }; + + ajv.compile(schema); + should.not.exist(output.warning); + }); + + it('should NOT throw an error or log a warning given an ignored default', function() { + var output = {}; + var ajv = new Ajv({ + useDefaults: true, + strictDefaults: false, + logger: getLogger(output) + }); + var schema = { + oneOf: [ + { enum: ['foo', 'bar'] }, + { + properties: { + foo: { + default: true + } + } + } + ] + }; + + ajv.compile(schema); + should.not.exist(output.warning); + }); + }); + + describe('strictDefaults = true', function() { + it('should throw an error given an ignored default in the schema root when strictDefaults is true', function() { + var ajv = new Ajv({useDefaults: true, strictDefaults: true}); + var schema = { + default: 5, + properties: {} + }; + should.throw(function() { ajv.compile(schema); }); + }); + + it('should throw an error given an ignored default in oneOf when strictDefaults is true', function() { + var ajv = new Ajv({useDefaults: true, strictDefaults: true}); + var schema = { + oneOf: [ + { enum: ['foo', 'bar'] }, + { + properties: { + foo: { + default: true + } + } + } + ] + }; + should.throw(function() { ajv.compile(schema); }); + }); + }); + + describe('strictDefaults = "log"', function() { + it('should log a warning given an ignored default in the schema root when strictDefaults is "log"', function() { + var output = {}; + var ajv = new Ajv({ + useDefaults: true, + strictDefaults: 'log', + logger: getLogger(output) + }); + var schema = { + default: 5, + properties: {} + }; + ajv.compile(schema); + should.equal(output.warning, 'default is ignored in the schema root'); + }); + + it('should log a warning given an ignored default in oneOf when strictDefaults is "log"', function() { + var output = {}; + var ajv = new Ajv({ + useDefaults: true, + strictDefaults: 'log', + logger: getLogger(output) + }); + var schema = { + oneOf: [ + { enum: ['foo', 'bar'] }, + { + properties: { + foo: { + default: true + } + } + } + ] + }; + ajv.compile(schema); + should.equal(output.warning, 'default is ignored for: data.foo'); + }); + }); + }); + + + describe('useDefaults = false', function() { + describe('strictDefaults = true', function() { + it('should NOT throw an error given an ignored default in the schema root when useDefaults is false', function() { + var ajv = new Ajv({useDefaults: false, strictDefaults: true}); + var schema = { + default: 5, + properties: {} + }; + should.not.throw(function() { ajv.compile(schema); }); + }); + + it('should NOT throw an error given an ignored default in oneOf when useDefaults is false', function() { + var ajv = new Ajv({useDefaults: false, strictDefaults: true}); + var schema = { + oneOf: [ + { enum: ['foo', 'bar'] }, + { + properties: { + foo: { + default: true + } + } + } + ] + }; + should.not.throw(function() { ajv.compile(schema); }); + }); + }); + }); + + + function getLogger(output) { + return { + log: function() { + throw new Error('log should not be called'); + }, + warn: function(warning) { + output.warning = warning; + }, + error: function() { + throw new Error('error should not be called'); + } + }; + } +}); diff --git a/spec/options/strictKeywords.spec.js b/spec/options/strictKeywords.spec.js new file mode 100644 index 000000000..4895b78e4 --- /dev/null +++ b/spec/options/strictKeywords.spec.js @@ -0,0 +1,79 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('strictKeywords option', function() { + describe('strictKeywords = false', function() { + it('should NOT throw an error or log a warning given an unknown keyword', function() { + var output = {}; + var ajv = new Ajv({ + strictKeywords: false, + logger: getLogger(output) + }); + var schema = { + properties: {}, + unknownKeyword: 1 + }; + + ajv.compile(schema); + should.not.exist(output.warning); + }); + }); + + describe('strictKeywords = true', function() { + it('should throw an error given an unknown keyword in the schema root when strictKeywords is true', function() { + var ajv = new Ajv({strictKeywords: true}); + var schema = { + properties: {}, + unknownKeyword: 1 + }; + should.throw(function() { ajv.compile(schema); }); + }); + }); + + describe('strictKeywords = "log"', function() { + it('should log a warning given an unknown keyword in the schema root when strictKeywords is "log"', function() { + var output = {}; + var ajv = new Ajv({ + strictKeywords: 'log', + logger: getLogger(output) + }); + var schema = { + properties: {}, + unknownKeyword: 1 + }; + ajv.compile(schema); + should.equal(output.warning, 'unknown keyword: unknownKeyword'); + }); + }); + + describe('unknown keyword inside schema that has no known keyword in compound keyword', function() { + it('should throw an error given an unknown keyword when strictKeywords is true', function() { + var ajv = new Ajv({strictKeywords: true}); + var schema = { + anyOf: [ + { + unknownKeyword: 1 + } + ] + }; + should.throw(function() { ajv.compile(schema); }); + }); + }); + + function getLogger(output) { + return { + log: function() { + throw new Error('log should not be called'); + }, + warn: function(warning) { + output.warning = warning; + }, + error: function() { + throw new Error('error should not be called'); + } + }; + } +}); diff --git a/spec/options/strictNumbers.spec.js b/spec/options/strictNumbers.spec.js new file mode 100644 index 000000000..adf63b026 --- /dev/null +++ b/spec/options/strictNumbers.spec.js @@ -0,0 +1,55 @@ +'use strict'; + +var Ajv = require('../ajv'); + +describe('structNumbers option', function() { + var ajv; + describe('strictNumbers default', testWithoutStrictNumbers(new Ajv())); + describe('strictNumbers = false', testWithoutStrictNumbers(new Ajv({strictNumbers: false}))); + describe('strictNumbers = true', function() { + beforeEach(function () { + ajv = new Ajv({strictNumbers: true}); + }); + + it('should fail validation for NaN/Infinity as type number', function() { + var validate = ajv.compile({type: 'number'}); + validate("1.1").should.equal(false); + validate(1.1).should.equal(true); + validate(1).should.equal(true); + validate(NaN).should.equal(false); + validate(Infinity).should.equal(false); + }); + + it('should fail validation for NaN as type integer', function() { + var validate = ajv.compile({type: 'integer'}); + validate("1.1").should.equal(false); + validate(1.1).should.equal(false); + validate(1).should.equal(true); + validate(NaN).should.equal(false); + validate(Infinity).should.equal(false); + }); + }); +}); + + +function testWithoutStrictNumbers(_ajv) { + return function () { + it('should NOT fail validation for NaN/Infinity as type number', function() { + var validate = _ajv.compile({type: 'number'}); + validate("1.1").should.equal(false); + validate(1.1).should.equal(true); + validate(1).should.equal(true); + validate(NaN).should.equal(true); + validate(Infinity).should.equal(true); + }); + + it('should NOT fail validation for NaN/Infinity as type integer', function() { + var validate = _ajv.compile({type: 'integer'}); + validate("1.1").should.equal(false); + validate(1.1).should.equal(false); + validate(1).should.equal(true); + validate(NaN).should.equal(false); + validate(Infinity).should.equal(true); + }); + }; +} diff --git a/spec/options/unknownFormats.spec.js b/spec/options/unknownFormats.spec.js new file mode 100644 index 000000000..81ea32655 --- /dev/null +++ b/spec/options/unknownFormats.spec.js @@ -0,0 +1,108 @@ +'use strict'; + +var Ajv = require('../ajv'); +var should = require('../chai').should(); + + +describe('unknownFormats option', function() { + describe('= true (default)', function() { + it('should fail schema compilation if unknown format is used', function() { + test(new Ajv); + test(new Ajv({unknownFormats: true})); + + function test(ajv) { + should.throw(function() { + ajv.compile({ format: 'unknown' }); + }); + } + }); + + it('should fail validation if unknown format is used via $data', function() { + test(new Ajv({$data: true})); + test(new Ajv({$data: true, unknownFormats: true})); + + function test(ajv) { + var validate = ajv.compile({ + properties: { + foo: { format: { $data: '1/bar' } }, + bar: { type: 'string' } + } + }); + + validate({foo: 1, bar: 'unknown'}) .should.equal(false); + validate({foo: '2016-10-16', bar: 'date'}) .should.equal(true); + validate({foo: '20161016', bar: 'date'}) .should.equal(false); + validate({foo: '20161016'}) .should.equal(true); + + validate({foo: '2016-10-16', bar: 'unknown'}) .should.equal(false); + } + }); + }); + + describe('= "ignore (default before 5.0.0)"', function() { + it('should pass schema compilation and be valid if unknown format is used', function() { + test(new Ajv({unknownFormats: 'ignore'})); + + function test(ajv) { + var validate = ajv.compile({ format: 'unknown' }); + validate('anything') .should.equal(true); + } + }); + + it('should be valid if unknown format is used via $data', function() { + test(new Ajv({$data: true, unknownFormats: 'ignore'})); + + function test(ajv) { + var validate = ajv.compile({ + properties: { + foo: { format: { $data: '1/bar' } }, + bar: { type: 'string' } + } + }); + + validate({foo: 1, bar: 'unknown'}) .should.equal(true); + validate({foo: '2016-10-16', bar: 'date'}) .should.equal(true); + validate({foo: '20161016', bar: 'date'}) .should.equal(false); + validate({foo: '20161016'}) .should.equal(true); + validate({foo: '2016-10-16', bar: 'unknown'}) .should.equal(true); + } + }); + }); + + describe('= [String]', function() { + it('should pass schema compilation and be valid if whitelisted unknown format is used', function() { + test(new Ajv({unknownFormats: ['allowed']})); + + function test(ajv) { + var validate = ajv.compile({ format: 'allowed' }); + validate('anything') .should.equal(true); + + should.throw(function() { + ajv.compile({ format: 'unknown' }); + }); + } + }); + + it('should be valid if whitelisted unknown format is used via $data', function() { + test(new Ajv({$data: true, unknownFormats: ['allowed']})); + + function test(ajv) { + var validate = ajv.compile({ + properties: { + foo: { format: { $data: '1/bar' } }, + bar: { type: 'string' } + } + }); + + validate({foo: 1, bar: 'allowed'}) .should.equal(true); + validate({foo: 1, bar: 'unknown'}) .should.equal(false); + validate({foo: '2016-10-16', bar: 'date'}) .should.equal(true); + validate({foo: '20161016', bar: 'date'}) .should.equal(false); + validate({foo: '20161016'}) .should.equal(true); + + validate({foo: '2016-10-16', bar: 'allowed'}) .should.equal(true); + validate({foo: '2016-10-16', bar: 'unknown'}) .should.equal(false); + } + }); + }); +}); diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.js new file mode 100644 index 000000000..7a12e8423 --- /dev/null +++ b/spec/options/useDefaults.spec.js @@ -0,0 +1,223 @@ +'use strict'; + +var Ajv = require('../ajv'); +var getAjvInstances = require('../ajv_instances'); +require('../chai').should(); + + +describe('useDefaults options', function() { + it('should replace undefined property with default value', function() { + var instances = getAjvInstances({ + allErrors: true, + loopRequired: 3 + }, { useDefaults: true }); + + instances.forEach(test); + + + function test(ajv) { + var schema = { + properties: { + foo: { type: 'string', default: 'abc' }, + bar: { type: 'number', default: 1 }, + baz: { type: 'boolean', default: false }, + nil: { type: 'null', default: null }, + obj: { type: 'object', default: {} }, + arr: { type: 'array', default: [] } + }, + required: ['foo', 'bar', 'baz', 'nil', 'obj', 'arr'], + minProperties: 6 + }; + + var validate = ajv.compile(schema); + + var data = {}; + validate(data) .should.equal(true); + data .should.eql({ foo: 'abc', bar: 1, baz: false, nil: null, obj: {}, arr:[] }); + + data = { foo: 'foo', bar: 2, obj: { test: true } }; + validate(data) .should.equal(true); + data .should.eql({ foo: 'foo', bar: 2, baz: false, nil: null, obj: { test: true }, arr:[] }); + } + }); + + it('should replace undefined item with default value', function() { + test(new Ajv({ useDefaults: true })); + test(new Ajv({ useDefaults: true, allErrors: true })); + + function test(ajv) { + var schema = { + items: [ + { type: 'string', default: 'abc' }, + { type: 'number', default: 1 }, + { type: 'boolean', default: false } + ], + minItems: 3 + }; + + var validate = ajv.compile(schema); + + var data = []; + validate(data) .should.equal(true); + data .should.eql([ 'abc', 1, false ]); + + data = [ 'foo' ]; + validate(data) .should.equal(true); + data .should.eql([ 'foo', 1, false ]); + + data = ['foo', 2,'false']; + validate(data) .should.equal(false); + validate.errors .should.have.length(1); + data .should.eql([ 'foo', 2, 'false' ]); + } + }); + + it('should apply default in "then" subschema (issue #635)', function() { + test(new Ajv({ useDefaults: true })); + test(new Ajv({ useDefaults: true, allErrors: true })); + + function test(ajv) { + var schema = { + if: { required: ['foo'] }, + then: { + properties: { + bar: { default: 2 } + } + }, + else: { + properties: { + foo: { default: 1 } + } + } + }; + + var validate = ajv.compile(schema); + + var data = {}; + validate(data) .should.equal(true); + data .should.eql({foo: 1}); + + data = {foo: 1}; + validate(data) .should.equal(true); + data .should.eql({foo: 1, bar: 2}); + } + }); + + + describe('useDefaults: by value / by reference', function() { + describe('using by value', function() { + it('should NOT modify underlying defaults when modifying validated data', function() { + test('value', new Ajv({ useDefaults: true })); + test('value', new Ajv({ useDefaults: true, allErrors: true })); + }); + }); + + describe('using by reference', function() { + it('should modify underlying defaults when modifying validated data', function() { + test('reference', new Ajv({ useDefaults: 'shared' })); + test('reference', new Ajv({ useDefaults: 'shared', allErrors: true })); + }); + }); + + function test(useDefaultsMode, ajv) { + var schema = { + properties: { + items: { + type: 'array', + default: ['a-default'] + } + } + }; + + var validate = ajv.compile(schema); + + var data = {}; + validate(data) .should.equal(true); + data.items .should.eql([ 'a-default' ]); + + data.items.push('another-value'); + data.items .should.eql([ 'a-default', 'another-value' ]); + + var data2 = {}; + validate(data2) .should.equal(true); + + if (useDefaultsMode == 'reference') + data2.items .should.eql([ 'a-default', 'another-value' ]); + else if (useDefaultsMode == 'value') + data2.items .should.eql([ 'a-default' ]); + else + throw new Error('unknown useDefaults mode'); + } + }); + + + describe('defaults with "empty" values', function() { + var schema, data; + + beforeEach(function() { + schema = { + properties: { + obj: { + properties: { + str: {default: 'foo'}, + n1: {default: 1}, + n2: {default: 2}, + n3: {default: 3} + } + }, + arr: { + items: [ + {default: 'foo'}, + {default: 1}, + {default: 2}, + {default: 3} + ] + } + } + }; + + data = { + obj: { + str: '', + n1: null, + n2: undefined + }, + arr: ['', null, undefined] + }; + }); + + it('should NOT assign defaults when useDefaults is true/"shared"', function() { + test(new Ajv({useDefaults: true})); + test(new Ajv({useDefaults: 'shared'})); + + function test(ajv) { + var validate = ajv.compile(schema); + validate(data) .should.equal(true); + data .should.eql({ + obj: { + str: '', + n1: null, + n2: 2, + n3: 3 + }, + arr: ['', null, 2, 3] + }); + } + }); + + it('should assign defaults when useDefaults = "empty"', function() { + var ajv = new Ajv({useDefaults: 'empty'}); + var validate = ajv.compile(schema); + validate(data) .should.equal(true); + data .should.eql({ + obj: { + str: 'foo', + n1: 1, + n2: 2, + n3: 3 + }, + arr: ['foo', 1, 2, 3] + }); + }); + }); +}); diff --git a/spec/remotes/bar.json b/spec/remotes/bar.json index 96ad644f7..cc3039186 100644 --- a/spec/remotes/bar.json +++ b/spec/remotes/bar.json @@ -1,4 +1,4 @@ { - "id": "http://localhost:1234/bar.json", + "$id": "http://localhost:1234/bar.json", "type": "string" } diff --git a/spec/remotes/buu.json b/spec/remotes/buu.json index 3f0f9af20..f3d905c4a 100644 --- a/spec/remotes/buu.json +++ b/spec/remotes/buu.json @@ -1,5 +1,5 @@ { - "id": "http://localhost:1234/buu.json", + "$id": "http://localhost:1234/buu.json", "definitions": { "buu": { "type": "object", diff --git a/spec/remotes/first.json b/spec/remotes/first.json index 7d414663f..9fdb8d486 100644 --- a/spec/remotes/first.json +++ b/spec/remotes/first.json @@ -1,4 +1,4 @@ { - "id": "http://localhost:1234/first.json", + "$id": "http://localhost:1234/first.json", "type": "string" } diff --git a/spec/remotes/foo.json b/spec/remotes/foo.json index b9a00dd42..9e565666f 100644 --- a/spec/remotes/foo.json +++ b/spec/remotes/foo.json @@ -1,5 +1,5 @@ { - "id": "http://localhost:1234/foo.json", + "$id": "http://localhost:1234/foo.json", "type": "object", "properties": { "bar": { "$ref": "bar.json" } diff --git a/spec/remotes/hyper-schema.json b/spec/remotes/hyper-schema.json index fee0cfb7e..349ee2de9 100644 --- a/spec/remotes/hyper-schema.json +++ b/spec/remotes/hyper-schema.json @@ -1,167 +1,69 @@ { - "$schema": "http://json-schema.org/draft-04/hyper-schema#", - "id": "http://json-schema.org/draft-04/hyper-schema#", + "$schema": "http://json-schema.org/draft-07/hyper-schema#", + "$id": "http://json-schema.org/draft-07/hyper-schema#", "title": "JSON Hyper-Schema", - "allOf": [ - { - "$ref": "http://json-schema.org/draft-04/schema#" - } - ], - "properties": { - "additionalItems": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#" - } - ] - }, - "additionalProperties": { - "anyOf": [ - { - "type": "boolean" - }, + "definitions": { + "schemaArray": { + "allOf": [ + { "$ref": "http://json-schema.org/draft-07/schema#/definitions/schemaArray" }, { - "$ref": "#" + "items": { "$ref": "#" } } ] - }, + } + }, + "allOf": [ { "$ref": "http://json-schema.org/draft-07/schema#" } ], + "properties": { + "additionalItems": { "$ref": "#" }, + "additionalProperties": { "$ref": "#"}, "dependencies": { "additionalProperties": { "anyOf": [ - { - "$ref": "#" - }, - { - "type": "array" - } + { "$ref": "#" }, + { "type": "array" } ] } }, "items": { "anyOf": [ - { - "$ref": "#" - }, - { - "$ref": "#/definitions/schemaArray" - } + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } ] }, "definitions": { - "additionalProperties": { - "$ref": "#" - } + "additionalProperties": { "$ref": "#" } }, "patternProperties": { - "additionalProperties": { - "$ref": "#" - } + "additionalProperties": { "$ref": "#" } }, "properties": { - "additionalProperties": { - "$ref": "#" - } - }, - "allOf": { - "$ref": "#/definitions/schemaArray" - }, - "anyOf": { - "$ref": "#/definitions/schemaArray" - }, - "oneOf": { - "$ref": "#/definitions/schemaArray" - }, - "not": { - "$ref": "#" - }, + "additionalProperties": { "$ref": "#" } + }, + "if": {"$ref": "#"}, + "then": {"$ref": "#"}, + "else": {"$ref": "#"}, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" }, + "contains": { "$ref": "#" }, + "propertyNames": { "$ref": "#" }, - "links": { - "type": "array", - "items": { - "$ref": "#/definitions/linkDescription" - } - }, - "fragmentResolution": { - "type": "string" - }, - "media": { - "type": "object", - "properties": { - "type": { - "description": "A media type, as described in RFC 2046", - "type": "string" - }, - "binaryEncoding": { - "description": "A content encoding scheme, as described in RFC 2045", - "type": "string" - } - } - }, - "pathStart": { - "description": "Instances' URIs must start with this value for this schema to apply to them", + "base": { "type": "string", - "format": "uri" - } - }, - "definitions": { - "schemaArray": { + "format": "uri-template" + }, + "links": { "type": "array", "items": { - "$ref": "#" - } - }, - "linkDescription": { - "title": "Link Description Object", - "type": "object", - "required": [ "href", "rel" ], - "properties": { - "href": { - "description": "a URI template, as defined by RFC 6570, with the addition of the $, ( and ) characters for pre-processing", - "type": "string" - }, - "rel": { - "description": "relation to the target resource of the link", - "type": "string" - }, - "title": { - "description": "a title for the link", - "type": "string" - }, - "targetSchema": { - "description": "JSON Schema describing the link target", - "$ref": "#" - }, - "mediaType": { - "description": "media type (as defined by RFC 2046) describing the link target", - "type": "string" - }, - "method": { - "description": "method for requesting the target of the link (e.g. for HTTP this might be \"GET\" or \"DELETE\")", - "type": "string" - }, - "encType": { - "description": "The media type in which to submit data along with the request", - "type": "string", - "default": "application/json" - }, - "schema": { - "description": "Schema describing the data to submit along with the request", - "$ref": "#" - } + "$ref": "http://json-schema.org/draft-07/hyper-schema#/links" } } }, "links": [ { "rel": "self", - "href": "{+id}" - }, - { - "rel": "full", - "href": "{+($ref)}" + "href": "{+%24id}" } ] } diff --git a/spec/remotes/node.json b/spec/remotes/node.json index d592c85ed..41120c1ef 100644 --- a/spec/remotes/node.json +++ b/spec/remotes/node.json @@ -1,5 +1,5 @@ { - "id": "http://localhost:1234/node.json", + "$id": "http://localhost:1234/node.json", "description": "node", "type": "object", "properties": { diff --git a/spec/remotes/scope_change.json b/spec/remotes/scope_change.json index 74ccbe853..67991f971 100644 --- a/spec/remotes/scope_change.json +++ b/spec/remotes/scope_change.json @@ -1,8 +1,8 @@ { - "id": "http://localhost:1234/scope_change.json", + "$id": "http://localhost:1234/scope_change.json", "definitions": { "foo": { - "id": "http://localhost:1234/scope_foo.json", + "$id": "http://localhost:1234/scope_foo.json", "definitions": { "bar": { "type": "string" @@ -10,7 +10,7 @@ } }, "baz": { - "id": "folder/", + "$id": "folder/", "type": "array", "items": { "$ref": "folderInteger.json" }, "bar": { diff --git a/spec/remotes/second.json b/spec/remotes/second.json index 06b4bc63e..56e32ebfd 100644 --- a/spec/remotes/second.json +++ b/spec/remotes/second.json @@ -1,5 +1,5 @@ { - "id": "http://localhost:1234/second.json", + "$id": "http://localhost:1234/second.json", "type": "object", "properties": { "first": { "$ref": "first.json" } diff --git a/spec/remotes/tree.json b/spec/remotes/tree.json index ba9d4cbf8..39df56141 100644 --- a/spec/remotes/tree.json +++ b/spec/remotes/tree.json @@ -1,5 +1,5 @@ { - "id": "http://localhost:1234/tree.json", + "$id": "http://localhost:1234/tree.json", "description": "tree of nodes", "type": "object", "properties": { diff --git a/spec/resolve.spec.js b/spec/resolve.spec.js index d1033d987..f04eabe96 100644 --- a/spec/resolve.spec.js +++ b/spec/resolve.spec.js @@ -20,28 +20,28 @@ describe('resolve', function () { it('should resolve ids in schema', function() { // Example from http://json-schema.org/latest/json-schema-core.html#anchor29 var schema = { - "id": "http://x.y.z/rootschema.json#", + "$id": "http://x.y.z/rootschema.json#", "schema1": { - "id": "#foo", + "$id": "#foo", "description": "schema1", "type": "integer" }, "schema2": { - "id": "otherschema.json", + "$id": "otherschema.json", "description": "schema2", "nested": { - "id": "#bar", + "$id": "#bar", "description": "nested", "type": "string" }, "alsonested": { - "id": "t/inner.json#a", + "$id": "t/inner.json#a", "description": "alsonested", "type": "boolean" } }, "schema3": { - "id": "some://where.else/completely#", + "$id": "some://where.else/completely#", "description": "schema3", "type": "null" }, @@ -65,13 +65,13 @@ describe('resolve', function () { it('should throw if the same id resolves to two different schemas', function() { instances.forEach(function (ajv) { ajv.compile({ - "id": "http://example.com/1.json", + "$id": "http://example.com/1.json", "type": "integer" }); should.throw(function() { ajv.compile({ "additionalProperties": { - "id": "http://example.com/1.json", + "$id": "http://example.com/1.json", "type": "string" } }); @@ -80,17 +80,46 @@ describe('resolve', function () { should.throw(function() { ajv.compile({ "items": { - "id": "#int", + "$id": "#int", "type": "integer" }, "additionalProperties": { - "id": "#int", + "$id": "#int", "type": "string" } }); }); }); }); + + it('should resolve ids defined as urn\'s (issue #423)', function() { + var schema = { + "type": "object", + "properties": { + "ip1": { + "$id": "urn:some:ip:prop", + "type": "string", + "format": "ipv4" + }, + "ip2": { + "$ref": "urn:some:ip:prop" + } + }, + "required": [ + "ip1", + "ip2" + ] + }; + + var data = { + "ip1": "0.0.0.0", + "ip2": "0.0.0.0" + }; + instances.forEach(function (ajv) { + var validate = ajv.compile(schema); + validate(data) .should.equal(true); + }); + }); }); @@ -98,7 +127,7 @@ describe('resolve', function () { it('should resolve fragment', function() { instances.forEach(function(ajv) { var schema = { - "id": "//e.com/types", + "$id": "//e.com/types", "definitions": { "int": { "type": "integer" } } @@ -166,7 +195,7 @@ describe('resolve', function () { instances.forEach(function (ajv) { try { ajv.compile({ - "id": opts.baseId, + "$id": opts.baseId, "properties": { "a": { "$ref": opts.ref } } }); } catch(e) { @@ -180,14 +209,14 @@ describe('resolve', function () { describe('inline referenced schemas without refs in them', function() { var schemas = [ - { id: 'http://e.com/obj.json#', + { $id: 'http://e.com/obj.json#', properties: { a: { $ref: 'int.json#' } } }, - { id: 'http://e.com/int.json#', + { $id: 'http://e.com/int.json#', type: 'integer', minimum: 2, maximum: 4 }, - { id: 'http://e.com/obj1.json#', + { $id: 'http://e.com/obj1.json#', definitions: { int: { type: 'integer', minimum: 2, maximum: 4 } }, properties: { a: { $ref: '#/definitions/int' } } }, - { id: 'http://e.com/list.json#', + { $id: 'http://e.com/list.json#', items: { $ref: 'obj.json#' } } ]; @@ -219,8 +248,8 @@ describe('resolve', function () { var ajv = new Ajv({ verbose: true }); var schemaMessage = { - $schema: "http://json-schema.org/draft-06/schema#", - id: "http://e.com/message.json#", + $schema: "http://json-schema.org/draft-07/schema#", + $id: "http://e.com/message.json#", type: "object", required: ["header"], properties: { @@ -235,8 +264,8 @@ describe('resolve', function () { // header schema var schemaHeader = { - $schema: "http://json-schema.org/draft-06/schema#", - id: "http://e.com/header.json#", + $schema: "http://json-schema.org/draft-07/schema#", + $id: "http://e.com/header.json#", type: "object", properties: { version: { @@ -270,14 +299,14 @@ describe('resolve', function () { var v = ajv.getSchema('http://e.com/message.json#'); v(validMessage) .should.equal(true); - v.schema.id .should.equal('http://e.com/message.json#'); + v.schema.$id .should.equal('http://e.com/message.json#'); v(invalidMessage) .should.equal(false); v.errors .should.have.length(1); - v.schema.id .should.equal('http://e.com/message.json#'); + v.schema.$id .should.equal('http://e.com/message.json#'); v(validMessage) .should.equal(true); - v.schema.id .should.equal('http://e.com/message.json#'); + v.schema.$id .should.equal('http://e.com/message.json#'); }); diff --git a/spec/schema-tests.spec.js b/spec/schema-tests.spec.js index bcca994c9..a07605710 100644 --- a/spec/schema-tests.spec.js +++ b/spec/schema-tests.spec.js @@ -36,9 +36,7 @@ jsonSchemaTest(instances, { ? suite(require('./tests/{**/,}*.json', {mode: 'list'})) : './tests/{**/,}*.json' }, - only: [ - // 'schemas/complex', 'schemas/basic', 'schemas/advanced', - ], + only: [], assert: require('./chai').assert, afterError: after.error, afterEach: after.each, @@ -48,7 +46,6 @@ jsonSchemaTest(instances, { function addRemoteRefs(ajv) { - ajv.addMetaSchema(require('../lib/refs/json-schema-draft-04.json')); for (var id in remoteRefs) ajv.addSchema(remoteRefs[id], id); ajv.addSchema(remoteRefsWithIds); } diff --git a/spec/security.spec.js b/spec/security.spec.js new file mode 100644 index 000000000..182e8b89f --- /dev/null +++ b/spec/security.spec.js @@ -0,0 +1,27 @@ +'use strict'; + +var jsonSchemaTest = require('json-schema-test') + , getAjvInstances = require('./ajv_instances') + , options = require('./ajv_options') + , suite = require('./browser_test_suite') + , after = require('./after_test'); + +var instances = getAjvInstances(options, { + schemas: [require('../lib/refs/json-schema-secure.json')] +}); + + +jsonSchemaTest(instances, { + description: 'Secure schemas tests of ' + instances.length + ' ajv instances with different options', + suites: { + 'security': typeof window == 'object' + ? suite(require('./security/{**/,}*.json', {mode: 'list'})) + : './security/{**/,}*.json' + }, + assert: require('./chai').assert, + afterError: after.error, + afterEach: after.each, + cwd: __dirname, + hideFolder: 'security/', + timeout: 90000 +}); diff --git a/spec/security/array.json b/spec/security/array.json new file mode 100644 index 000000000..25b655f95 --- /dev/null +++ b/spec/security/array.json @@ -0,0 +1,104 @@ +[ + { + "description": "uniqueItems without type keyword should be used together with maxItems", + "schema": {"$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#"}, + "tests": [ + { + "description": "uniqueItems keyword used without maxItems is unsafe", + "data": { + "uniqueItems": true + }, + "valid": false + }, + { + "description": "uniqueItems keyword used with maxItems is safe", + "data": { + "uniqueItems": true, + "maxItems": "10" + }, + "valid": true + }, + { + "description": "uniqueItems: false is ignored (and safe)", + "data": { + "uniqueItems": false + }, + "valid": true + } + ] + }, + { + "description": "uniqueItems with scalar type(s) is safe to use without maxItems", + "schema": {"$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#"}, + "tests": [ + { + "description": "uniqueItems keyword with a single scalar type is safe", + "data": { + "uniqueItems": true, + "items": { + "type": "number" + } + }, + "valid": true + }, + { + "description": "uniqueItems keyword with multiple scalar types is safe", + "data": { + "uniqueItems": true, + "items": { + "type": ["number", "string"] + } + }, + "valid": true + } + ] + }, + { + "description": "uniqueItems with compound type(s) should be used together with maxItems", + "schema": {"$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#"}, + "tests": [ + { + "description": "uniqueItems keyword with a single compound type and without maxItems is unsafe", + "data": { + "uniqueItems": true, + "items": { + "type": "object" + } + }, + "valid": false + }, + { + "description": "uniqueItems keyword with a single compound type and with maxItems is safe", + "data": { + "uniqueItems": true, + "maxItems": "10", + "items": { + "type": "object" + } + }, + "valid": true + }, + { + "description": "uniqueItems keyword with multiple types including compound type and without maxItems is unsafe", + "data": { + "uniqueItems": true, + "items": { + "type": ["array","number"] + } + }, + "valid": false + }, + { + "description": "uniqueItems keyword with multiple types including compound type and with maxItems is safe", + "data": { + "uniqueItems": true, + "maxItems": "10", + "items": { + "type": ["array","number"] + } + }, + "valid": true + } + ] + } +] diff --git a/spec/security/object.json b/spec/security/object.json new file mode 100644 index 000000000..9de069b8a --- /dev/null +++ b/spec/security/object.json @@ -0,0 +1,29 @@ +[ + { + "description": "patternProperties keyword should be used together with propertyNames", + "schema": { "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" }, + "tests": [ + { + "description": "patternProperties keyword used without propertyNames is unsafe", + "data": { + "patternProperties": { + ".*": {} + } + }, + "valid": false + }, + { + "description": "patternProperties keyword used with propertyNames is safe", + "data": { + "patternProperties": { + ".*": {} + }, + "propertyNames": { + "maxLength": "256" + } + }, + "valid": true + } + ] + } +] diff --git a/spec/security/string.json b/spec/security/string.json new file mode 100644 index 000000000..c6fb55292 --- /dev/null +++ b/spec/security/string.json @@ -0,0 +1,44 @@ +[ + { + "description": "pattern keyword should be used together with maxLength", + "schema": { "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" }, + "tests": [ + { + "description": "pattern keyword used without maxLength is unsafe", + "data": { + "pattern": ".*" + }, + "valid": false + }, + { + "description": "pattern keyword used with maxLength is safe", + "data": { + "pattern": ".*", + "maxLength": "256" + }, + "valid": true + } + ] + }, + { + "description": "format keyword should be used together with maxLength", + "schema": { "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" }, + "tests": [ + { + "description": "format keyword used without maxLength is unsafe", + "data": { + "format": "email" + }, + "valid": false + }, + { + "description": "format keyword used with maxLength is safe", + "data": { + "format": "email", + "maxLength": "256" + }, + "valid": true + } + ] + } +] diff --git a/spec/tests/issues/1061_alternative_time_offsets.json b/spec/tests/issues/1061_alternative_time_offsets.json new file mode 100644 index 000000000..bf80f9581 --- /dev/null +++ b/spec/tests/issues/1061_alternative_time_offsets.json @@ -0,0 +1,74 @@ +[ + { + "description": "Support for alternative ISO-8601 timezone offset formats (#1061)", + "schema": {"format": "date-time"}, + "tests": [ + { + "description": "valid positiive two digit", + "data": "2016-01-31T02:31:17+01", + "valid": true + }, + { + "description": "valid negative two digit", + "data": "2016-01-31T02:31:17-01", + "valid": true + }, + { + "description": "valid positiive four digit no colon", + "data": "2016-01-31T02:31:17+0130", + "valid": true + }, + { + "description": "valid negative four digit no colon", + "data": "2016-01-31T02:31:17-0130", + "valid": true + }, + { + "description": "invalid positiive three digit no colon", + "data": "2016-01-31T02:31:17+013", + "valid": false + }, + { + "description": "invalid negative three digit no colon", + "data": "2016-01-31T02:31:17-013", + "valid": false + } + ] + }, + { + "description": "Support for alternative ISO-8601 timezone offset formats (#1061)", + "schema": {"format": "time"}, + "tests": [ + { + "description": "valid positiive two digit", + "data": "02:31:17+01", + "valid": true + }, + { + "description": "valid negative two digit", + "data": "02:31:17-01", + "valid": true + }, + { + "description": "valid positiive four digit no colon", + "data": "02:31:17+0130", + "valid": true + }, + { + "description": "valid negative four digit no colon", + "data": "02:31:17-0130", + "valid": true + }, + { + "description": "invalid positiive three digit no colon", + "data": "02:31:17+013", + "valid": false + }, + { + "description": "invalid negative three digit no colon", + "data": "02:31:17-013", + "valid": false + } + ] + } +] diff --git a/spec/tests/issues/13_root_ref_in_ref_in_remote_ref.json b/spec/tests/issues/13_root_ref_in_ref_in_remote_ref.json index 5947cc590..a55625abb 100644 --- a/spec/tests/issues/13_root_ref_in_ref_in_remote_ref.json +++ b/spec/tests/issues/13_root_ref_in_ref_in_remote_ref.json @@ -1,22 +1,13 @@ [ { "description": "root ref in remote ref (#13)", - "schemas": [ - { - "id": "http://localhost:1234/issue13_1", - "type": "object", - "properties": { - "name": { "$ref": "name.json#/definitions/orNull" } - } - }, - { - "$id": "http://localhost:1234/issue13_2", - "type": "object", - "properties": { - "name": { "$ref": "name.json#/definitions/orNull" } - } + "schema": { + "$id": "http://localhost:1234/issue13", + "type": "object", + "properties": { + "name": { "$ref": "name.json#/definitions/orNull" } } - ], + }, "tests": [ { "description": "string is valid", diff --git a/spec/tests/issues/14_ref_in_remote_ref_with_id.json b/spec/tests/issues/14_ref_in_remote_ref_with_id.json index ae9f3f802..57b474a4b 100644 --- a/spec/tests/issues/14_ref_in_remote_ref_with_id.json +++ b/spec/tests/issues/14_ref_in_remote_ref_with_id.json @@ -1,18 +1,11 @@ [ { "description": "ref in remote ref with ids", - "schemas": [ - { - "id": "http://localhost:1234/issue14a_1.json", - "type": "array", - "items": { "$ref": "foo.json" } - }, - { - "$id": "http://localhost:1234/issue14a_2.json", - "type": "array", - "items": { "$ref": "foo.json" } - } - ], + "schema": { + "$id": "http://localhost:1234/issue14a.json", + "type": "array", + "items": { "$ref": "foo.json" } + }, "tests": [ { "description": "string is valid", @@ -36,18 +29,11 @@ }, { "description": "remote ref in definitions in remote ref with ids (#14)", - "schemas": [ - { - "id": "http://localhost:1234/issue14b_1.json", - "type": "array", - "items": { "$ref": "buu.json#/definitions/buu" } - }, - { - "$id": "http://localhost:1234/issue14b_2.json", - "type": "array", - "items": { "$ref": "buu.json#/definitions/buu" } - } - ], + "schema": { + "$id": "http://localhost:1234/issue14b.json", + "type": "array", + "items": { "$ref": "buu.json#/definitions/buu" } + }, "tests": [ { "description": "string is valid", diff --git a/spec/tests/issues/170_ref_and_id_in_sibling.json b/spec/tests/issues/170_ref_and_id_in_sibling.json index eb4876850..bb08d8294 100644 --- a/spec/tests/issues/170_ref_and_id_in_sibling.json +++ b/spec/tests/issues/170_ref_and_id_in_sibling.json @@ -3,12 +3,12 @@ "description": "sibling property has id (#170)", "schemas": [ { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "http://example.com/base_object_1", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://example.com/base_object_1", "type": "object", "properties": { "title": { - "id": "http://example.com/title", + "$id": "http://example.com/title", "type": "string" }, "file": { "$ref": "#/definitions/file-entry" } @@ -18,7 +18,7 @@ } }, { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://example.com/base_object_2", "type": "object", "properties": { @@ -56,12 +56,12 @@ "description": "sibling item has id", "schemas": [ { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "http://example.com/base_array_1", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://example.com/base_array_1", "type": "array", "items": [ { - "id": "http://example.com/0", + "$id": "http://example.com/0", "type": "string" }, { "$ref": "#/definitions/file-entry" } @@ -71,7 +71,7 @@ } }, { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://example.com/base_array_2", "type": "array", "items": [ @@ -103,11 +103,11 @@ "description": "sibling schema in anyOf has id", "schemas": [ { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "http://example.com/base_anyof_1", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://example.com/base_anyof_1", "anyOf": [ { - "id": "http://example.com/0", + "$id": "http://example.com/0", "type": "number" }, { "$ref": "#/definitions/def" } @@ -117,7 +117,7 @@ } }, { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://example.com/base_anyof_2", "anyOf": [ { @@ -153,11 +153,11 @@ "description": "sibling schema in oneOf has id", "schemas": [ { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "http://example.com/base_oneof_1", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://example.com/base_oneof_1", "oneOf": [ { - "id": "http://example.com/0", + "$id": "http://example.com/0", "type": "number" }, { "$ref": "#/definitions/def" } @@ -167,7 +167,7 @@ } }, { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://example.com/base_oneof_2", "oneOf": [ { @@ -203,11 +203,11 @@ "description": "sibling schema in allOf has id", "schemas": [ { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "http://example.com/base_allof_1", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://example.com/base_allof_1", "allOf": [ { - "id": "http://example.com/0", + "$id": "http://example.com/0", "type": "string", "maxLength": 3 }, @@ -218,7 +218,7 @@ } }, { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://example.com/base_allof_2", "allOf": [ { @@ -250,12 +250,12 @@ "description": "sibling schema in dependencies has id", "schemas": [ { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "http://example.com/base_dependencies_1", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://example.com/base_dependencies_1", "type": "object", "dependencies": { "foo": { - "id": "http://example.com/foo", + "$id": "http://example.com/foo", "required": [ "bar" ] }, "bar": { "$ref": "#/definitions/def" } @@ -265,7 +265,7 @@ } }, { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://example.com/base_dependencies_2", "type": "object", "dependencies": { diff --git a/spec/tests/issues/1_ids_in_refs.json b/spec/tests/issues/1_ids_in_refs.json index 5c6eed1f1..579e7d072 100644 --- a/spec/tests/issues/1_ids_in_refs.json +++ b/spec/tests/issues/1_ids_in_refs.json @@ -5,7 +5,7 @@ { "definitions": { "int": { - "id": "#int", + "$id": "#int", "type": "integer" } }, @@ -30,10 +30,10 @@ "description": "IDs in refs with root id", "schemas": [ { - "id": "http://example.com/int_1.json", + "$id": "http://example.com/int_1.json", "definitions": { "int": { - "id": "#int", + "$id": "#int", "type": "integer" } }, diff --git a/spec/tests/issues/27_1_recursive_raml_schema.json b/spec/tests/issues/27_1_recursive_raml_schema.json index 4ae4089e9..e2146b2d9 100644 --- a/spec/tests/issues/27_1_recursive_raml_schema.json +++ b/spec/tests/issues/27_1_recursive_raml_schema.json @@ -1,9 +1,9 @@ [ { - "description": "JSON schema for a standard RAML object (#27)", + "description": "JSON Schema for a standard RAML object (#27)", "schema": { - "title": "A JSON schema for a standard RAML object", - "$schema": "http://json-schema.org/draft-04/schema#", + "title": "A JSON Schema for a standard RAML object", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "title" diff --git a/spec/tests/issues/27_recursive_reference.json b/spec/tests/issues/27_recursive_reference.json index fb9a542cc..6da686979 100644 --- a/spec/tests/issues/27_recursive_reference.json +++ b/spec/tests/issues/27_recursive_reference.json @@ -3,12 +3,12 @@ "description": "Recursive reference (#27)", "schemas": [ { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "testrec_1", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "testrec_1", "type": "object", "properties": { "layout": { - "id": "layout", + "$id": "layout", "type": "object", "properties": { "layout": { "type": "string" }, @@ -30,7 +30,7 @@ } }, { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "$id": "testrec_2", "type": "object", "properties": { diff --git a/spec/tests/issues/63_id_property_not_in_schema.json b/spec/tests/issues/63_id_property_not_in_schema.json index 8d9d36e63..ce425cc3f 100644 --- a/spec/tests/issues/63_id_property_not_in_schema.json +++ b/spec/tests/issues/63_id_property_not_in_schema.json @@ -1,20 +1,12 @@ [ { "description": "id property in referenced schema in object that is not a schema (#63)", - "schemas": [ - { - "type" : "object", - "properties": { - "title": { "$ref": "http://json-schema.org/draft-04/schema#/properties/title" } - } - }, - { - "type" : "object", - "properties": { - "title": { "$ref": "http://json-schema.org/draft-06/schema#/properties/title" } - } + "schema": { + "type" : "object", + "properties": { + "title": { "$ref": "http://json-schema.org/draft-07/schema#/properties/title" } } - ], + }, "tests": [ { "description": "empty object is valid", diff --git a/spec/tests/issues/70_1_recursive_hash_ref_in_remote_ref.json b/spec/tests/issues/70_1_recursive_hash_ref_in_remote_ref.json index 6f2c21166..651e3d019 100644 --- a/spec/tests/issues/70_1_recursive_hash_ref_in_remote_ref.json +++ b/spec/tests/issues/70_1_recursive_hash_ref_in_remote_ref.json @@ -2,7 +2,7 @@ { "description": "hash ref inside hash ref in remote ref (#70, was passing)", "schema": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" }, "tests": [ { "data": 1, "valid": true, "description": "positive integer is valid" }, @@ -12,16 +12,10 @@ }, { "description": "hash ref inside hash ref in remote ref with id (#70, was passing)", - "schemas": [ - { - "id": "http://example.com/my_schema_1.json", - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" - }, - { - "$id": "http://example.com/my_schema_2.json", - "$ref": "http://json-schema.org/draft-06/schema#/definitions/nonNegativeIntegerDefault0" - } - ], + "schema": { + "$id": "http://example.com/my_schema_2.json", + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" + }, "tests": [ { "data": 1, "valid": true, "description": "positive integer is valid" }, { "data": 0, "valid": true, "description": "zero is valid" }, @@ -32,7 +26,7 @@ "description": "local hash ref with remote hash ref without inner hash ref (#70, was passing)", "schema": { "definitions": { - "a": { "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" } + "a": { "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" } }, "properties": { "b": { "$ref": "#/definitions/a" } @@ -48,7 +42,7 @@ "description": "local hash ref with remote hash ref that has inner hash ref (#70)", "schema": { "definitions": { - "a": { "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" } + "a": { "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" } }, "properties": { "b": { "$ref": "#/definitions/a" } diff --git a/spec/tests/issues/70_swagger_schema.json b/spec/tests/issues/70_swagger_schema.json index 0c97800bc..3c9a2c80d 100644 --- a/spec/tests/issues/70_swagger_schema.json +++ b/spec/tests/issues/70_swagger_schema.json @@ -3,8 +3,8 @@ "description": "Swagger api schema does not compile (#70)", "schema": { "title": "A JSON Schema for Swagger 2.0 API.", - "id": "http://swagger.io/v2/schema.json#", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "http://swagger.io/v2/schema.json#", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "swagger", @@ -935,58 +935,58 @@ "type": "string" }, "title": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + "$ref": "http://json-schema.org/draft-07/schema#/properties/title" }, "description": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + "$ref": "http://json-schema.org/draft-07/schema#/properties/description" }, "default": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + "$ref": "http://json-schema.org/draft-07/schema#/properties/default" }, "multipleOf": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + "$ref": "http://json-schema.org/draft-07/schema#/properties/multipleOf" }, "maximum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/maximum" }, "exclusiveMaximum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/exclusiveMaximum" }, "minimum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/minimum" }, "exclusiveMinimum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/exclusiveMinimum" }, "maxLength": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger" }, "minLength": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" }, "pattern": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + "$ref": "http://json-schema.org/draft-07/schema#/properties/pattern" }, "maxItems": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger" }, "minItems": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" }, "uniqueItems": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + "$ref": "http://json-schema.org/draft-07/schema#/properties/uniqueItems" }, "maxProperties": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger" }, "minProperties": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" }, "required": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/stringArray" }, "enum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/enum" }, "additionalProperties": { "anyOf": [ @@ -1000,7 +1000,7 @@ "default": {} }, "type": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/type" + "$ref": "http://json-schema.org/draft-07/schema#/properties/type" }, "items": { "anyOf": [ @@ -1064,16 +1064,16 @@ "type": "string" }, "title": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + "$ref": "http://json-schema.org/draft-07/schema#/properties/title" }, "description": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + "$ref": "http://json-schema.org/draft-07/schema#/properties/description" }, "default": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + "$ref": "http://json-schema.org/draft-07/schema#/properties/default" }, "required": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/stringArray" }, "type": { "type": "string", @@ -1534,49 +1534,49 @@ "default": "csv" }, "title": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + "$ref": "http://json-schema.org/draft-07/schema#/properties/title" }, "description": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + "$ref": "http://json-schema.org/draft-07/schema#/properties/description" }, "default": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + "$ref": "http://json-schema.org/draft-07/schema#/properties/default" }, "multipleOf": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + "$ref": "http://json-schema.org/draft-07/schema#/properties/multipleOf" }, "maximum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/maximum" }, "exclusiveMaximum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/exclusiveMaximum" }, "minimum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/minimum" }, "exclusiveMinimum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/exclusiveMinimum" }, "maxLength": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger" }, "minLength": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" }, "pattern": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + "$ref": "http://json-schema.org/draft-07/schema#/properties/pattern" }, "maxItems": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger" }, "minItems": { - "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" }, "uniqueItems": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + "$ref": "http://json-schema.org/draft-07/schema#/properties/uniqueItems" }, "enum": { - "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + "$ref": "http://json-schema.org/draft-07/schema#/properties/enum" }, "jsonReference": { "type": "object", diff --git a/spec/tests/issues/861_empty_propertynames.json b/spec/tests/issues/861_empty_propertynames.json new file mode 100644 index 000000000..55ce3501f --- /dev/null +++ b/spec/tests/issues/861_empty_propertynames.json @@ -0,0 +1,23 @@ +[ + { + "description": "propertyNames with empty schema (#861)", + "schema": { + "properties": { + "foo": {"type": "string"} + }, + "propertyNames": {} + }, + "tests": [ + { + "description": "valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "invalid", + "data": {"foo": 1}, + "valid": false + } + ] + } +] diff --git a/spec/tests/rules/comment.json b/spec/tests/rules/comment.json new file mode 100644 index 000000000..5be030e4f --- /dev/null +++ b/spec/tests/rules/comment.json @@ -0,0 +1,40 @@ +[ + { + "description": "$comment keyword", + "schema": { + "$comment": "test" + }, + "tests": [ + { + "description": "any value is valid", + "data": 1, + "valid": true + } + ] + }, + { + "description": "$comment keyword in subschemas", + "schema": { + "type": "object", + "properties": { + "foo": { + "$comment": "test" + } + } + }, + "tests": [ + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "any value of property foo is valid object is valid", + "data": { + "foo": 1 + }, + "valid": true + } + ] + } +] diff --git a/spec/tests/rules/format.json b/spec/tests/rules/format.json index 4eb9c9ded..c316a6ee5 100644 --- a/spec/tests/rules/format.json +++ b/spec/tests/rules/format.json @@ -82,10 +82,55 @@ "data": "123.example.com", "valid": true }, + { + "description": "valid hostname - trailing dot", + "data": "123.example.com.", + "valid": true + }, + { + "description": "valid hostname - single label", + "data": "localhost", + "valid": true + }, + { + "description": "valid hostname - single label with trailing dot", + "data": "localhost.", + "valid": true + }, { "description": "valid hostname #312", "data": "lead-routing-qa.lvuucj.0001.use1.cache.amazonaws.com", "valid": true + }, + { + "description": "valid hostname - maximum length label (63 chars)", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.example.com", + "valid": true + }, + { + "description": "invalid hostname - label too long (64 chars)", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.example.com", + "valid": false + }, + { + "description": "valid hostname - maximum length hostname (255 octets)", + "data": "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxy.example.com", + "valid": true + }, + { + "description": "valid hostname - maximum length hostname (255 octets) with trailing dot", + "data": "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxy.example.com.", + "valid": true + }, + { + "description": "invalid hostname - hostname too long (256 octets)", + "data": "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.example.com", + "valid": false + }, + { + "description": "invalid hostname - hostname too long (256 octets) with trailing dot", + "data": "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.example.com.", + "valid": false } ] }, @@ -511,9 +556,19 @@ "valid": true }, { - "description": "not valid time", + "description": "an invalid time format", "data": "12.34.56", "valid": false + }, + { + "description": "an invalid time", + "data": "12:34:67", + "valid": false + }, + { + "description": "a valid time (leap second)", + "data": "23:59:60", + "valid": true } ] }, @@ -535,6 +590,21 @@ "description": "an invalid date-time string (additional part)", "data": "1963-06-19T12:13:14ZTinvalid", "valid": false + }, + { + "description": "an invalid date-time string (invalid date)", + "data": "1963-20-19T12:13:14Z", + "valid": false + }, + { + "description": "an invalid date-time string (invalid time)", + "data": "1963-06-19T12:13:67Z", + "valid": false + }, + { + "description": "a valid date-time string (leap second)", + "data": "2016-12-31T23:59:60Z", + "valid": true } ] }, @@ -568,11 +638,6 @@ "data": "/foo/bar~0/baz~1/%a", "valid": true }, - { - "description": "a valid JSON-pointer as uri fragment", - "data": "#/foo/%25a", - "valid": true - }, { "description": "empty string is valid", "data": "", @@ -588,11 +653,6 @@ "data": "/foo/bar~", "valid": false }, - { - "description": "not a valid JSON-pointer as uri fragment (% not URL-encoded)", - "data": "#/foo/%a", - "valid": false - }, { "description": "valid JSON-pointer with empty segment", "data": "/foo//bar", @@ -602,6 +662,22 @@ "description": "valid JSON-pointer with the last empty segment", "data": "/foo/bar/", "valid": true + } + ] + }, + { + "description": "validation of JSON-pointer URI fragment strings", + "schema": {"format": "json-pointer-uri-fragment"}, + "tests": [ + { + "description": "a valid JSON-pointer as uri fragment", + "data": "#/foo/%25a", + "valid": true + }, + { + "description": "not a valid JSON-pointer as uri fragment (% not URL-encoded)", + "data": "#/foo/%a", + "valid": false }, { "description": "valid JSON-pointer with empty segment as uri fragment", diff --git a/spec/tests/rules/if.json b/spec/tests/rules/if.json new file mode 100644 index 000000000..87e9e7bc2 --- /dev/null +++ b/spec/tests/rules/if.json @@ -0,0 +1,134 @@ +[ + { + "description": "if/then keyword validation", + "schema": { + "if": { "minimum": 10 }, + "then": { "multipleOf": 2 } + }, + "tests": [ + { + "description": ">= 10 and even is valid", + "data": 12, + "valid": true + }, + { + "description": ">= 10 and odd is invalid", + "data": 11, + "valid": false + }, + { + "description": "< 10 is valid", + "data": 9, + "valid": true + } + ] + }, + { + "description": "if/then/else keyword validation", + "schema": { + "if": { "maximum": 10 }, + "then": { "multipleOf": 2 }, + "else": { "multipleOf": 5 } + }, + "tests": [ + { + "description": "<=10 and even is valid", + "data": 8, + "valid": true + }, + { + "description": "<=10 and odd is invalid", + "data": 7, + "valid": false + }, + { + "description": ">10 and mulitple of 5 is valid", + "data": 15, + "valid": true + }, + { + "description": ">10 and not mulitple of 5 is invalid", + "data": 17, + "valid": false + } + ] + }, + { + "description": "if keyword with id in sibling subschema", + "schema": { + "$id": "http://example.com/base_if", + "if": { + "$id": "http://example.com/if", + "minimum": 10 + }, + "then": { "$ref": "#/definitions/def" }, + "definitions": { + "def": { "multipleOf": 2 } + } + }, + "tests": [ + { + "description": ">= 10 and even is valid", + "data": 12, + "valid": true + }, + { + "description": ">= 10 and odd is invalid", + "data": 11, + "valid": false + }, + { + "description": "< 10 is valid", + "data": 9, + "valid": true + } + ] + }, + { + "description": "then/else without if should be ignored", + "schema": { + "then": { "multipleOf": 2 }, + "else": { "multipleOf": 5 } + }, + "tests": [ + { + "description": "even is valid", + "data": 8, + "valid": true + }, + { + "description": "odd is valid", + "data": 7, + "valid": true + }, + { + "description": "mulitple of 5 is valid", + "data": 15, + "valid": true + }, + { + "description": "not mulitple of 5 is valid", + "data": 17, + "valid": true + } + ] + }, + { + "description": "if without then/else should be ignored", + "schema": { + "if": { "maximum": 10 } + }, + "tests": [ + { + "description": "<=10 is valid", + "data": 8, + "valid": true + }, + { + "description": ">10 is valid", + "data": 15, + "valid": true + } + ] + } +] diff --git a/spec/tests/rules/uniqueItems.json b/spec/tests/rules/uniqueItems.json new file mode 100644 index 000000000..d283d2045 --- /dev/null +++ b/spec/tests/rules/uniqueItems.json @@ -0,0 +1,113 @@ +[ + { + "description": "uniqueItems with algorithm using hash", + "schema": { + "items": { "type": "string" }, + "uniqueItems": true + }, + "tests": [ + { + "description": "array of unique strings is valid", + "data": ["foo", "bar", "baz"], + "valid": true + }, + { + "description": "array of unique items with strings that are properties of hash is valid", + "data": ["toString", "foo"], + "valid": true + }, + { + "description": "array of non-unique strings is invalid", + "data": ["foo", "bar", "bar"], + "valid": false + }, + { + "description": "array with non-strings is invalid", + "data": ["1", 2], + "valid": false + } + ] + }, + { + "description": "uniqueItems with multiple types when the list of types includes array", + "schema": { + "items": { "type": ["array", "string"] }, + "uniqueItems": true + }, + "tests": [ + { + "description": "array of unique items is valid", + "data": [[1], [2], "foo"], + "valid": true + }, + { + "description": "array of non-unique items is invalid", + "data": [[1], [1], "foo"], + "valid": false + }, + { + "description": "array with incorrect type is invalid", + "data": [{}, 1, 2], + "valid": false + } + ] + }, + { + "description": "uniqueItems with multiple types when the list of types includes object", + "schema": { + "items": { "type": ["object", "string"] }, + "uniqueItems": true + }, + "tests": [ + { + "description": "array of unique items is valid", + "data": [{"a": 1}, {"b": 2}, "foo"], + "valid": true + }, + { + "description": "array of non-unique items is invalid", + "data": [{"a": 1}, {"a": 1}, "foo"], + "valid": false + }, + { + "description": "array with incorrect type is invalid", + "data": [[], 1, 2], + "valid": false + } + ] + }, + { + "description": "uniqueItems with multiple types when all types are scalar", + "schema": { + "items": { "type": ["number", "string", "boolean", "null"] }, + "uniqueItems": true + }, + "tests": [ + { + "description": "array of unique items is valid (string/number)", + "data": ["1", 1, 2], + "valid": true + }, + { + "description": "array of unique items is valid (string/boolean)", + "data": ["true", true, false], + "valid": true + }, + { + "description": "array of unique items is valid (string/null)", + "data": ["null", null, 0], + "valid": true + }, + { + "description": "array of non-unique items is invalid", + "data": [1, 1, 2], + "valid": false + }, + { + "description": "array with incorrect type is invalid", + "data": [[], 1, 2], + "valid": false + } + ] + } +] diff --git a/spec/tests/schemas/advanced.json b/spec/tests/schemas/advanced.json index 12e94fc1f..7c6cea9ad 100644 --- a/spec/tests/schemas/advanced.json +++ b/spec/tests/schemas/advanced.json @@ -2,7 +2,7 @@ { "description": "advanced schema from z-schema benchmark (https://github.com/zaggino/z-schema)", "schema": { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "/": { "$ref": "#/definitions/entry" } @@ -14,7 +14,7 @@ "required": [ "/" ], "definitions": { "entry": { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "description": "schema for an fstab entry", "type": "object", "required": [ "storage" ], diff --git a/spec/tests/schemas/basic.json b/spec/tests/schemas/basic.json index 1ab3c4a71..a65006908 100644 --- a/spec/tests/schemas/basic.json +++ b/spec/tests/schemas/basic.json @@ -2,7 +2,7 @@ { "description": "basic schema from z-schema benchmark (https://github.com/zaggino/z-schema)", "schema": { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "title": "Product set", "type": "array", "items": { @@ -18,8 +18,7 @@ }, "price": { "type": "number", - "minimum": 0, - "exclusiveMinimum": true + "exclusiveMinimum": 0 }, "tags": { "type": "array", diff --git a/spec/tests/schemas/complex.json b/spec/tests/schemas/complex.json index 68a95619b..46d15446b 100644 --- a/spec/tests/schemas/complex.json +++ b/spec/tests/schemas/complex.json @@ -7,17 +7,17 @@ "minItems": 1, "definitions": { "base58": { - "id": "#base58", + "$id": "#base58", "type": "string", "pattern": "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$" }, "hex": { - "id": "#hex", + "$id": "#hex", "type": "string", "pattern": "^[0123456789A-Fa-f]+$" }, "tx_id": { - "id": "#tx_id", + "$id": "#tx_id", "allOf": [ { "$ref": "#hex" }, { @@ -27,7 +27,7 @@ ] }, "address": { - "id": "#address", + "$id": "#address", "allOf": [ { "$ref": "#base58" }, { @@ -37,7 +37,7 @@ ] }, "signature": { - "id": "#signature", + "$id": "#signature", "allOf": [ { "$ref": "#hex" }, { @@ -47,7 +47,7 @@ ] }, "transaction": { - "id": "#transaction", + "$id": "#transaction", "additionalProperties": false, "required": [ "metadata", @@ -108,7 +108,7 @@ } }, "input": { - "id": "#input", + "$id": "#input", "type": "object", "additionalProperties": false, "required": [ @@ -134,7 +134,7 @@ } }, "output": { - "id": "#output", + "$id": "#output", "type": "object", "additionalProperties": false, "required": [ diff --git a/spec/tests/schemas/complex3.json b/spec/tests/schemas/complex3.json index 13d6375df..1591d7814 100644 --- a/spec/tests/schemas/complex3.json +++ b/spec/tests/schemas/complex3.json @@ -2,23 +2,23 @@ { "description": "complex schema from jsck benchmark (https://github.com/pandastrike/jsck)", "schema": { - "id": "http://example.com/complex3.json", + "$id": "http://example.com/complex3.json", "type": "array", "items": { "$ref": "#transaction" }, "minItems": 1, "definitions": { "base58": { - "id": "#base58", + "$id": "#base58", "type": "string", "pattern": "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$" }, "hex": { - "id": "#hex", + "$id": "#hex", "type": "string", "pattern": "^[0123456789A-Fa-f]+$" }, "tx_id": { - "id": "#tx_id", + "$id": "#tx_id", "allOf": [ { "$ref": "#hex" }, { @@ -28,7 +28,7 @@ ] }, "address": { - "id": "#address", + "$id": "#address", "allOf": [ { "$ref": "#base58" }, { @@ -38,7 +38,7 @@ ] }, "signature": { - "id": "#signature", + "$id": "#signature", "allOf": [ { "$ref": "#hex" }, { @@ -48,7 +48,7 @@ ] }, "transaction": { - "id": "#transaction", + "$id": "#transaction", "additionalProperties": false, "required": [ "metadata", @@ -109,7 +109,7 @@ } }, "input": { - "id": "#input", + "$id": "#input", "type": "object", "additionalProperties": false, "required": [ @@ -135,7 +135,7 @@ } }, "output": { - "id": "#output", + "$id": "#output", "type": "object", "additionalProperties": false, "required": [ diff --git a/spec/typescript/index.ts b/spec/typescript/index.ts new file mode 100644 index 000000000..0858180e9 --- /dev/null +++ b/spec/typescript/index.ts @@ -0,0 +1,60 @@ +import ajv = require("../.."); + +// #region new() +const options: ajv.Options = { + verbose: true, +}; + +let instance: ajv.Ajv; + +instance = ajv(); +instance = ajv(options); + +instance = new ajv(); +instance = new ajv(options); +// #endregion new() + +// #region validate() +let data = { + foo: 42, +} + +let result = instance.validate("", data); + +if (typeof result === "boolean") { + // sync + console.log(result); +} else { + // async + result.then(value => { + data = value; + }); +} +// #endregion validate() + +// #region compile() +const validator = instance.compile({}); +result = validator(data); + +if (typeof result === "boolean") { + // sync + console.log(result); +} else { + // async + result.then(value => { + data = value; + }); +} +// #endregion compile() + +// #region errors +const validationError: ajv.ValidationError = new ajv.ValidationError([]); +validationError instanceof ajv.ValidationError; +validationError.ajv === true; +validationError.validation === true; + +ajv.MissingRefError.message("", ""); +const missingRefError: ajv.MissingRefError = new ajv.MissingRefError("", "", ""); +missingRefError instanceof ajv.MissingRefError; +missingRefError.missingRef; +// #endregion