Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ajv-validator/ajv
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v8.8.2
Choose a base ref
...
head repository: ajv-validator/ajv
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v8.9.0
Choose a head ref
  • 13 commits
  • 19 files changed
  • 9 contributors

Commits on Nov 21, 2021

  1. build(deps-dev): bump lint-staged from 11.2.6 to 12.1.1 (#1827)

    Bumps [lint-staged](https://github.com/okonet/lint-staged) from 11.2.6 to 12.1.1.
    - [Release notes](https://github.com/okonet/lint-staged/releases)
    - [Commits](lint-staged/lint-staged@v11.2.6...v12.1.1)
    
    ---
    updated-dependencies:
    - dependency-name: lint-staged
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 21, 2021
    Copy the full SHA
    8d20f6b View commit details

Commits on Dec 14, 2021

  1. Unverified

    The email in this signature doesn’t match the committer email.
    Copy the full SHA
    c7bf889 View commit details

Commits on Dec 15, 2021

  1. doc: fix typo (#1847)

    "Additionaly" should be "Additionally".
    
    Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
    eventualbuddha and epoberezkin authored Dec 15, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    e9df4c1 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    84d0a57 View commit details
  3. add logo

    epoberezkin authored Dec 15, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    43ed019 View commit details
  4. Update ReDoS section of security.md to accommodate #1683 (#1828)

    * Update ReDoS section of security.md
    
    * Update docs/security.md
    
    * Update docs/security.md
    
    * Update docs/security.md
    
    Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
    efebarlas and epoberezkin authored Dec 15, 2021
    Copy the full SHA
    c3e203c View commit details

Commits on Dec 17, 2021

  1. build(deps-dev): bump @types/node from 16.11.14 to 17.0.0 (#1851)

    Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 16.11.14 to 17.0.0.
    - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
    - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)
    
    ---
    updated-dependencies:
    - dependency-name: "@types/node"
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Dec 17, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    d21fa70 View commit details

Commits on Dec 22, 2021

  1. Add option to generate ESM exports instead of CJS (#1523) (#1861)

    * feat: add option to generate ESM exports instead of CJS (#1523)
    
    * feat: add option to generate ESM exports instead of CJS (#1523)
    - Renamed exportEsm to esm
    - Extracted common code
    - Changed invalid export names to rather throw an error
    rehanvdm authored Dec 22, 2021
    Copy the full SHA
    418cd0f View commit details

Commits on Jan 15, 2022

  1. Update getting-started.md (#1871)

    Minor spelling and grammar corrections.
    thernstig authored Jan 15, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    d632539 View commit details
  2. Fix broken named anchor link (#1870)

    Prefix the closing angle bracket with a forward slash so that a correct stub is generated for table of contents.
    justinpowers authored Jan 15, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    f2e590a View commit details
  3. Updated standalone documentation and add new examples (#1866)

    * docs: updated standalone documentation and added new examples #1831
    
    * remove unnecessary line breaks
    
    * remove style changes
    
    * minor corrections
    
    * minor corrections
    
    * Update docs/standalone.md
    
    * Update docs/standalone.md
    
    * Update docs/standalone.md
    
    Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
    rehanvdm and epoberezkin authored Jan 15, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ec96c87 View commit details
  4. resolve references before validating the discriminator (#1815)

    * resolve references before validating the discriminator
    fixes #1554
    
    * prettier style
    
    * adjust according to comments, add some doc
    
    * resolve schema from SchemaEnv
    
    * simplify code, change comment
    
    * add import
    
    * Update lib/vocabularies/discriminator/index.ts
    
    * let to conts
    
    * Update lib/vocabularies/discriminator/index.ts
    
    * update error message
    
    * fix regexp in the test
    
    Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
    dfeufel and epoberezkin authored Jan 15, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    e7bc009 View commit details
  5. 8.9.0

    epoberezkin committed Jan 15, 2022
    Copy the full SHA
    979d46b View commit details
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ Supports JSON Schema draft-04/06/07/2019-09/2020-12 ([draft-04 support](https://

[<img src="https://ajv.js.org/img/microsoft.png" width="31%" alt="Microsoft">](https://opensource.microsoft.com)<img src="https://ajv.js.org/img/gap.svg" width="3%">[<img src="https://ajv.js.org/img/reserved.svg" width="31%">](https://opencollective.com/ajv)<img src="https://ajv.js.org/img/gap.svg" width="3%">[<img src="https://ajv.js.org/img/reserved.svg" width="31%">](https://opencollective.com/ajv)

[<img src="https://ajv.js.org/img/retool.svg" width="22.5%" alt="Retool">](https://retool.com/?utm_source=sponsor&utm_campaign=ajv)<img src="https://ajv.js.org/img/gap.svg" width="3%">[<img src="https://ajv.js.org/img/tidelift.svg" width="22.5%" alt="Tidelift">](https://tidelift.com/subscription/pkg/npm-ajv?utm_source=npm-ajv&utm_medium=referral&utm_campaign=enterprise)<img src="https://ajv.js.org/img/gap.svg" width="3%">[<img src="https://ajv.js.org/img/reserved.svg" width="22.5%">](https://opencollective.com/ajv)<img src="https://ajv.js.org/img/gap.svg" width="3%">[<img src="https://ajv.js.org/img/reserved.svg" width="22.5%">](https://opencollective.com/ajv)
[<img src="https://ajv.js.org/img/retool.svg" width="22.5%" alt="Retool">](https://retool.com/?utm_source=sponsor&utm_campaign=ajv)<img src="https://ajv.js.org/img/gap.svg" width="3%">[<img src="https://ajv.js.org/img/tidelift.svg" width="22.5%" alt="Tidelift">](https://tidelift.com/subscription/pkg/npm-ajv?utm_source=npm-ajv&utm_medium=referral&utm_campaign=enterprise)<img src="https://ajv.js.org/img/gap.svg" width="3%">[<img src="https://ajv.js.org/img/simplex.svg" width="22.5%" alt="SimpleX">](https://github.com/simplex-chat/simplex-chat)<img src="https://ajv.js.org/img/gap.svg" width="3%">[<img src="https://ajv.js.org/img/reserved.svg" width="22.5%">](https://opencollective.com/ajv)

## Contributing

25 changes: 25 additions & 0 deletions docs/.vuepress/public/img/simplex.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -53,9 +53,9 @@ Use JSON Type Definition or JSON Schema

<Sponsors level="bronze">

[![retool](/img/retool.svg)](https://retool.com/?utm_source=sponsor&utm_campaign=ajv)
[![tidelift](/img/tidelift.svg)](https://tidelift.com/subscription/pkg/npm-ajv?utm_source=npm-ajv&utm_medium=referral&utm_campaign=enterprise)
[![reserved](/img/reserved.svg)](https://opencollective.com/ajv)
[![Retool](/img/retool.svg)](https://retool.com/?utm_source=sponsor&utm_campaign=ajv)
[![Tidelift](/img/tidelift.svg)](https://tidelift.com/subscription/pkg/npm-ajv?utm_source=npm-ajv&utm_medium=referral&utm_campaign=enterprise)
[![SimpleX](/img/simplex.svg)](https://github.com/simplex-chat/simplex-chat)
[![reserved](/img/reserved.svg)](https://opencollective.com/ajv)

</Sponsors>
9 changes: 9 additions & 0 deletions docs/guide/environments.md
Original file line number Diff line number Diff line change
@@ -94,6 +94,15 @@ const ajv = new Ajv({code: {es5: true}})

See [Advanced options](https://github.com/ajv-validator/ajv/blob/master/docs/api.md#advanced-options).

## CJS vs ESM exports

The default configuration of AJV is to generate code in ES6 with Common JS (CJS) exports. This can be changed by setting
the ES Modules(ESM) flag.

```javascript
const ajv = new Ajv({code: {esm: true}})
```

## Other JavaScript environments

Ajv is used in other JavaScript environments, including Electron apps, WeChat mini-apps and many others, where the same considerations apply as above:
8 changes: 4 additions & 4 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ See [Contributing](../CONTRIBUTING.md) on how to run the tests locally
## Basic data validation

Ajv takes a schema for your JSON data and converts it into a very efficient JavaScript code
that validates your data according to the schema. To create schema you can use either
that validates your data according to the schema. To create a schema you can use either
[JSON Schema](../json-schema) or [JSON Type Definition](../json-type-definition) - check out [Choosing schema language](./schema-language), they have
different advantages and disadvantages.

@@ -87,7 +87,7 @@ if (!valid) console.log(validate.errors)
</code-block>
</code-group>

Ajv compiles schemas to functions and caches them in all cases (using schema itself as a key for Map), so that the next time the same schema object is used it won't be compiled again.
Ajv compiles schemas to functions and caches them in all cases (using the schema itself as a key in a Map), so that the next time the same schema object is used it won't be compiled again.

::: tip Best performance: compile and getSchema methods
The best performance is achieved when using compiled functions returned by `compile` or `getSchema` methods.
@@ -98,7 +98,7 @@ re-use compiled validation functions. See [Managing multiple schemas](./managing
:::

::: warning Save errors property
Every time a validation function (or `ajv.validate`) is 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](../api.md#validation-errors)
Every time a validation function (or `ajv.validate`) is called the `errors` property is overwritten. You need to copy the `errors` array reference to another variable if you want to use it later (e.g. in the callback). See [Validation errors](../api.md#validation-errors).
:::

## Parsing and serializing JSON <Badge text="New" />
@@ -107,7 +107,7 @@ Ajv can compile efficient parsers and serializers from [JSON Type Definition](..

Serializing the data with a function specialized to your data shape can be more than 10x compared with `JSON.stringify`.

Parsing the data replaces the need for a separate validation after generic parsing with `JSON.parse` (although validation itself is usually much faster than parsing). In case your JSON string is valid specialized parsing is approximately as fast as JSON.parse, but in case your JSON is invalid, specialized parsing would fail much faster - so it can be very efficient in some scenarios.
Parsing the data replaces the need for separate validation after generic parsing with `JSON.parse` (although validation itself is usually much faster than parsing). In case your JSON string is valid, the specialized parsing is approximately as fast as JSON.parse, but in case your JSON is invalid, the specialized parsing would fail much faster - so it can be very efficient in some scenarios.

For the same data structure, you can compile parser and serializer in this way:

2 changes: 1 addition & 1 deletion docs/guide/typescript.md
Original file line number Diff line number Diff line change
@@ -279,7 +279,7 @@ JSON Type Definition only supports tagged unions, so unions in JTD are fully sup
JSON Schema is more complex and so `JSONSchemaType` has limited support for type safe unions.

`JSONSchemaType` will type check unions where each union element is fully specified as an element of an `anyOf` array or `oneOf` array.
Additionaly, unions of primitives will type check appropriately if they're combined into an array `type`, e.g. `{type: ["string", "number"]}`.
Additionally, unions of primitives will type check appropriately if they're combined into an array `type`, e.g. `{type: ["string", "number"]}`.

::: warning TypeScript limitation
Note that due to current limitation of TypeScript, JSONSchemaType cannot verify that every element of the union is present, and the following example is still valid `const schema: JSONSchemaType<number | string> = {type: "string"}`.
4 changes: 2 additions & 2 deletions docs/json-schema.md
Original file line number Diff line number Diff line change
@@ -992,7 +992,7 @@ There are following requirements and limitations of using `discriminator` keywor
- `mapping` in discriminator object is not supported.
- [oneOf](#oneof) keyword must be present in the same schema.
- discriminator property should be [requried](#required) either on the top level, as in the example, or in all `oneOf` subschemas.
- each `oneOf` subschema must have [properties](#properties) keyword with discriminator property.
- each `oneOf` subschema must have [properties](#properties) keyword with discriminator property. The subschemas should be either inlined or included as direct references (only `$ref` keyword without any extra keywords is allowed).
- schema for discriminator property in each `oneOf` subschema must be [const](#const) or [enum](#enum), with unique values across all subschemas.

Not meeting any of these requirements would fail schema compilation.
@@ -1160,7 +1160,7 @@ If the data is invalid against the sub-schema in `if` keyword, then the validati
then: {multipleOf: 100},
else: {
if: {minimum: 10},
then": {multipleOf: 10}
then: {multipleOf: 10}
}
}
```
12 changes: 8 additions & 4 deletions docs/options.md
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ const defaultOptions = {
code: {
// NEW
es5: false,
esm: false,
lines: false,
source: false,
process: undefined, // (code: string) => string
@@ -184,25 +185,25 @@ Option values:
- `true` (default) - use unicode flag "u".
- `false` - do not use flag "u".

### timestamp <Badge text="JTD only">
### timestamp <Badge text="JTD only" />

Defines which Javascript types will be accepted for the [JTD timestamp type](./json-type-definition#type-form).

By default Ajv will accept both Date objects and [RFC3339](https://datatracker.ietf.org/doc/rfc3339/) strings. You can specify allowed values with the option `timestamp: "date"` or `timestamp: "string"`.

### parseDate <Badge text="JTD only">
### parseDate <Badge text="JTD only" />

Defines how date-time strings are parsed by [JTD parsers](./api.md#jtd-parse). By default Ajv parses date-time strings as string. Use `parseDate: true` to parse them as Date objects.

### allowDate <Badge text="JTD only">
### allowDate <Badge text="JTD only" />

Defines how date-time strings are parsed and validated. By default Ajv only allows full date-time strings, as required by JTD specification. Use `allowDate: true` to allow date strings both for validation and for parsing.

::: warning Option allowDate is not portable
This option makes JTD validation and parsing more permissive and non-standard. The date strings without time part will be accepted by Ajv, but will be rejected by other JTD validators.
:::

### int32range <Badge text="JTD only">
### int32range <Badge text="JTD only" />

Can be used to disable range checking for `int32` and `uint32` types.

@@ -347,6 +348,9 @@ Code generation options:
```typescript
type CodeOptions = {
es5?: boolean // to generate es5 code - by default code is es6, with "for-of" loops, "let" and "const"
esm?: boolean // how functions should be exported - by default CJS is used, so the validate function(s)
// file can be `required`. Set this value to true to export the validate function(s) as ES Modules, enabling
// bunlers to do their job.
lines?: boolean // add line-breaks to code - to simplify debugging of generated functions
source?: boolean // add `source` property (see Source below) to validating function.
process?: (code: string, schema?: SchemaEnv) => string // an optional function to process generated code
20 changes: 20 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
@@ -65,6 +65,26 @@ Certain regular expressions can lead to the exponential evaluation time even wit

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.

By default, Ajv uses the regex engine built into Node.js. This engine has exponential worst-case performance. This performance (and ReDoS attacks) can be mitigated by using a linear-time regex engine. Ajv supports the use of a third-party regex engine for this purpose.

To use a third-party regex engine in Ajv, set the ajv.opts.code.regExp property to that regex engine during instantiation. Here we use Google’s RE2 engine as an example.

```
const Ajv = require("ajv")
const RE2 = require("re2")
const ajv = new Ajv({regExp: RE2})
```

For details about the interface of the `regexp` option, see options.md under the docs folder.

Although linear-time regex engines eliminate ReDoS vulnerabilities, changing a regex engine carries some risk, including:

- Minor changes in regex syntax.
- Minor changes in regex semantics. For example, RE2 always interprets regexes in Unicode, and disagrees with JavaScript in its definition of whitespace. To avoid regressions, develop and test your regexes in the same regex engine that you use in production.
- May not support some advanced features, such as look-aheads or back-references.
- May have (minor) common-case performance degradation.
- Increases size of distributable (e.g. RE2 includes a non-trivial C component).

::: warning ReDoS attack
Some formats that [ajv-formats](https://github.com/ajv-validator/ajv-formats) package implements use [regular expressions](https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts) that can be vulnerable to ReDoS attack.
:::
223 changes: 190 additions & 33 deletions docs/standalone.md
Original file line number Diff line number Diff line change
@@ -2,83 +2,241 @@

[[toc]]

Ajv supports generating standalone modules with exported validation function(s), with one default export or multiple named exports, that are pre-compiled and can be used without Ajv. It is useful for several reasons:
Ajv supports generating standalone validation functions from JSON Schemas at compile/build time. These functions can then be used during runtime to do validation without initialising Ajv. It is useful for several reasons:

- to reduce the browser bundle size - Ajv is not included in the bundle (although if you have a large number of schemas the bundle can become bigger - when the total size of generated validation code is bigger than Ajv code).
- to reduce the start-up time - the validation and compilation of schemas will happen during build time.
- to avoid dynamic code evaluation with Function constructor (used for schema compilation) - when it is prohibited by the browser page [Content Security Policy](./security.md#content-security-policy).

This functionality in Ajv v7 supersedes deprecated package ajv-pack that can be used with Ajv v6. All schemas, including those with recursive references, formats and user-defined keywords are supported, with few [limitations](#configuration-and-limitations).
This functionality in Ajv v7 supersedes the deprecated package ajv-pack that can be used with Ajv v6. All schemas, including those with recursive references, formats and user-defined keywords are supported, with few [limitations](#configuration-and-limitations).

## Usage with CLI
## Two-step process

In most cases you would use this functionality via [ajv-cli](https://github.com/ajv-validator/ajv-cli) (>= 4.0.0) to generate module that exports validation function.
The **first step** is to **generate** the standalone validation function code. This is done at compile/build time of your project and the output is a generated JS file. The **second step** is to **use** the generated JS validation function.

There are two methods to generate the code, using either the Ajv CLI or the Ajv JS library. There are also a few different options that can be passed when generating code. Below is just a highlight of a few options:

- Set the `code.source` (JS) value to true or use the `compile` (CLI) command to generate standalone code.
- The standalone code can be generated in either ES5 or ES6, it defaults to ES6. Set the `code.es5` (JS) value to true or pass the `--code-es5` (CLI) flag to true if you want ES5 code.
- The standalone code can be generated in either CJS (module.export & require) or ESM (exports & import), it defaults to CJS. Set the `code.esm` (JS) value to true or pass the `--code-esm` (CLI) flag if you want ESM exported code.

Note that the way the function is exported, differs if you are exporting a single or multiple schemas. See examples below.

### Generating function(s) using CLI

In most cases you would use this functionality via [ajv-cli](https://github.com/ajv-validator/ajv-cli) (>= 4.0.0) to generate the standalone code that exports the validation function. See [ajv-cli](https://github.com/ajv-validator/ajv-cli#compile-schemas) docs and the [cli options](https://github.com/ajv-validator/ajv-cli#ajv-options) for additional information.

#### Using the defaults - ES6 and CJS exports
```sh
npm install -g ajv-cli
ajv compile -s schema.json -o validate_schema.js
```

`validate_schema.js` will contain the module exporting validation function that can be bundled into your application.

See [ajv-cli](https://github.com/ajv-validator/ajv-cli) docs for additional information.

## Usage from code
### Generating using the JS library

Install the package, version >= v7.0.0:
```sh
npm install ajv
```

#### Generating functions(s) for a single schema using the JS library - ES6 and CJS exports

```javascript
const Ajv = require("ajv") // version >= v7.0.0
const ajv = new Ajv({code: {source: true}}) // this option is required to generate standalone code
const fs = require("fs")
const path = require("path")
const Ajv = require("ajv")
const standaloneCode = require("ajv/dist/standalone").default

const schema = {
$id: "https://example.com/object.json",
$id: "https://example.com/bar.json",
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
foo: {
type: "string",
pattern: "^[a-z]+$",
},
bar: {type: "string"},
},
"required": ["bar"]
}

// 1. generate module with a single default export (CommonJS and ESM compatible):
// The generated code will have a default export:
// `module.exports = <validateFunctionCode>;module.exports.default = <validateFunctionCode>;`
const ajv = new Ajv({code: {source: true}})
const validate = ajv.compile(schema)
let moduleCode = standaloneCode(ajv, validate)

// 2. pass map of schema IDs to generate multiple exports,
// it avoids code duplication if schemas are mutually recursive or have some share elements:
let moduleCode = standaloneCode(ajv, {
validateObject: "https://example.com/object.json",
})
// Now you can write the module code to file
fs.writeFileSync(path.join(__dirname, "../consume/validate-cjs.js"), moduleCode)
```

#### Generating functions(s) for multiple schemas using the JS library - ES6 and CJS exports

```javascript
const fs = require("fs")
const path = require("path")
const Ajv = require("ajv")
const standaloneCode = require("ajv/dist/standalone").default

// 3. or generate module with all schemas added to the instance (excluding meta-schemas),
// export names would use schema IDs (or keys passed to addSchema method):
const schemaFoo = {
$id: "#/definitions/Foo",
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
foo: {"$ref": "#/definitions/Bar"}
}
}
const schemaBar = {
$id: "#/definitions/Bar",
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
bar: {type: "string"},
},
"required": ["bar"]
}

// For CJS, it generates an exports array, will generate
// `exports["#/definitions/Foo"] = ...;exports["#/definitions/Bar"] = ... ;`
const ajv = new Ajv({schemas: [schemaFoo, schemaBar], code: {source: true}})
let moduleCode = standaloneCode(ajv)

// now you can
// write module code to file
// Now you can write the module code to file
fs.writeFileSync(path.join(__dirname, "../consume/validate-cjs.js"), moduleCode)
```

#### Generating functions(s) for multiple schemas using the JS library - ES6 and ESM exports

```javascript
const fs = require("fs")
const path = require("path")
fs.writeFileSync(path.join(__dirname, "/validate.js"), moduleCode)
const Ajv = require("ajv")
const standaloneCode = require("ajv/dist/standalone").default

const schemaFoo = {
$id: "#/definitions/Foo",
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
foo: {"$ref": "#/definitions/Bar"}
}
}
const schemaBar = {
$id: "#/definitions/Bar",
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
bar: {type: "string"},
},
"required": ["bar"]
}

// ... or require module from string
const requireFromString = require("require-from-string")
const standaloneValidate = requireFromString(moduleCode) // for a single default export
// For ESM, the export name needs to be a valid export name, it can not be `export const #/definitions/Foo = ...;` so we
// need to provide a mapping between a valid name and the $id field. Below will generate
// `export const Foo = ...;export const Bar = ...;`
// This mapping would not have been needed if the `$ids` was just `Bar` and `Foo` instead of `#/definitions/Foo`
// and `#/definitions/Bar` respectfully
const ajv = new Ajv({schemas: [schemaFoo, schemaBar], code: {source: true, esm: true}})
let moduleCode = standaloneCode(ajv, {
"Foo": "#/definitions/Foo",
"Bar": "#/definitions/Bar"
})

// Now you can write the module code to file
fs.writeFileSync(path.join(__dirname, "../consume/validate-esm.mjs"), moduleCode)
```

::: warning ESM name mapping
The ESM version only requires the mapping if the ids are not valid export names. If the $ids were just the
`Foo` and `Bar` instead of `#/definitions/Foo` and `#/definitions/Bar` then the mapping would not be needed.
:::


## Using the validation function(s)

### Validating a single schemas using the JS library - ES6 and CJS

```javascript
const Bar = require('./validate-cjs')

const barPass = {
bar: "something"
}

const barFail = {
// bar: "something" // <= empty/omitted property that is required
}

let validateBar = Bar
if (!validateBar(barPass))
console.log("ERRORS 1:", validateBar.errors) //Never reaches this because valid

if (!validateBar(barFail))
console.log("ERRORS 2:", validateBar.errors) //Errors array gets logged
```

### Validating multiple schemas using the JS library - ES6 and CJS

```javascript
const validations = require('./validate-cjs')

const fooPass = {
foo: {
bar: "something"
}
}

const fooFail = {
foo: {
// bar: "something" // <= empty/omitted property that is required
}
}

let validateFoo = validations["#/definitions/Foo"];
if (!validateFoo(fooPass))
console.log("ERRORS 1:", validateFoo.errors); //Never reaches this because valid

if (!validateFoo(fooFail))
console.log("ERRORS 2:", validateFoo.errors); //Errors array gets logged

```

### Validating multiple schemas using the JS library - ES6 and ESM

```javascript
import {Foo, Bar} from './validate-multiple-esm.mjs';

const fooPass = {
foo: {
bar: "something"
}
}

const fooFail = {
foo: {
// bar: "something" // bar: "something" <= empty properties
}
}

let validateFoo = Foo;
if (!validateFoo(fooPass))
console.log("ERRORS 1:", validateFoo.errors); //Never reaches here because valid

if (!validateFoo(fooFail))
console.log("ERRORS 2:", validateFoo.errors); //Errors array gets logged
```


### Requirement at runtime

To run the standalone generated functions, the Ajv package should still be a run-time dependency for most schemas, but generated modules can only depend on code in [runtime](https://github.com/ajv-validator/ajv/tree/master/lib/runtime) folder, so the whole Ajv will not be included in the bundle (or executed) if you require the modules with standalone validation code from your application code.
One of the main reason for using the standalone mode is to start applications faster to avoid runtime schema compilation.

The standalone generated functions still has a dependency on the Ajv. Specifically on the code in the [runtime](https://github.com/ajv-validator/ajv/tree/master/lib/runtime) folder of the package.

Completely isolated validation functions can be generated if desired (won't be for most use cases). Run the generated code through a bundler like ES Build to create completely isolated validation functions that can be imported/required without any dependency on Ajv. This is also not needed if your project is already using a bundler.

## Configuration and limitations

To support standalone code generation:

- Ajv option `source.code` must be set to `true`
- Ajv option `code.source` must be set to `true`
- only `code` and `macro` user-defined keywords are supported (see [User defined keywords](./keywords.md)).
- when `code` keywords define variables in shared scope using `gen.scopeValue`, they must provide `code` property with the code snippet. See source code of pre-defined keywords for examples in [vocabularies folder](https://github.com/ajv-validator/ajv/blob/master/lib/vocabularies).
- if formats are used in standalone code, ajv option `code.formats` should contain the code snippet that will evaluate to an object with all used format definitions - it can be a call to `require("...")` with the correct path (relative to the location of saved module):
@@ -93,6 +251,5 @@ const ajv = new Ajv({
formats: _`require("./my-formats")`,
},
})
```

If you only use formats from [ajv-formats](https://github.com/ajv-validator/ajv-formats) this option will be set by this package automatically.
8 changes: 8 additions & 0 deletions lib/compile/codegen/code.ts
Original file line number Diff line number Diff line change
@@ -155,6 +155,14 @@ export function getProperty(key: Code | string | number): Code {
return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]`
}

//Does best effort to format the name properly
export function getEsmExportName(key: Code | string | number): Code {
if (typeof key == "string" && IDENTIFIER.test(key)) {
return new _Code(`${key}`)
}
throw new Error(`CodeGen: invalid export name: ${key}, use explicit $id name mapping`)
}

export function regexpCode(rx: RegExp): Code {
return new _Code(rx.toString())
}
1 change: 1 addition & 0 deletions lib/core.ts
Original file line number Diff line number Diff line change
@@ -140,6 +140,7 @@ export interface CurrentOptions {

export interface CodeOptions {
es5?: boolean
esm?: boolean
lines?: boolean
optimize?: boolean | number
formats?: Code // code to require (or construct) map of available formats - for standalone code
11 changes: 9 additions & 2 deletions lib/standalone/index.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import type AjvCore from "../core"
import type {AnyValidateFunction, SourceCode} from "../types"
import type {SchemaEnv} from "../compile"
import {UsedScopeValues, UsedValueState, ValueScopeName, varKinds} from "../compile/codegen/scope"
import {_, nil, _Code, Code, getProperty} from "../compile/codegen/code"
import {_, nil, _Code, Code, getProperty, getEsmExportName} from "../compile/codegen/code"

function standaloneCode(
ajv: AjvCore,
@@ -30,6 +30,10 @@ function standaloneCode(
const usedValues: UsedScopeValues = {}
const n = source?.validateName
const vCode = validateCode(usedValues, source)
if (ajv.opts.code.esm) {
// Always do named export as `validate` rather than the variable `n` which is `validateXX` for known export value
return `"use strict";${_n}export const validate = ${n};${_n}export default ${n};${_n}${vCode}`
}
return `"use strict";${_n}module.exports = ${n};${_n}module.exports.default = ${n};${_n}${vCode}`
}

@@ -43,7 +47,10 @@ function standaloneCode(
const v = getValidateFunc(schemas[name] as T)
if (v) {
const vCode = validateCode(usedValues, v.source)
code = _`${code}${_n}exports${getProperty(name)} = ${v.source?.validateName};${_n}${vCode}`
const exportSyntax = ajv.opts.code.esm
? _`export const ${getEsmExportName(name)}`
: _`exports${getProperty(name)}`
code = _`${code}${_n}${exportSyntax} = ${v.source?.validateName};${_n}${vCode}`
}
}
return `${code}`
5 changes: 1 addition & 4 deletions lib/types/json-schema.ts
Original file line number Diff line number Diff line change
@@ -113,10 +113,7 @@ type UncheckedJSONSchemaType<T, IsPartial extends boolean> = (
dependentSchemas?: {[K in keyof T]?: UncheckedPartialSchema<T>}
minProperties?: number
maxProperties?: number
} & (// "required" type does not guarantee that all required properties
// are listed it only asserts that optional cannot be listed.
// "required" is not necessary if it's a non-partial type with no required keys
IsPartial extends true
} & (IsPartial extends true // "required" is not necessary if it's a non-partial type with no required keys // are listed it only asserts that optional cannot be listed. // "required" type does not guarantee that all required properties
? {required: Readonly<(keyof T)[]>}
: [UncheckedRequiredMembers<T>] extends [never]
? {required?: Readonly<UncheckedRequiredMembers<T>[]>}
9 changes: 4 additions & 5 deletions lib/types/jtd-schema.ts
Original file line number Diff line number Diff line change
@@ -117,9 +117,8 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
: never))
// empty - empty schemas also treat nullable differently in that it's now fully ignored
| (unknown extends T ? {nullable?: boolean} : never)
// all other types
| ((// numbers - only accepts the type number
true extends NullTypeEquality<T, number>
// all other types // numbers - only accepts the type number
| ((true extends NullTypeEquality<T, number>
? {type: NumberType}
: // booleans - accepts the type boolean
true extends NullTypeEquality<T, boolean>
@@ -198,8 +197,8 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
}

type JTDDataDef<S, D extends Record<string, unknown>> =
| (// ref
S extends {ref: string}
| // ref
(S extends {ref: string}
? D extends {[K in S["ref"]]: infer V}
? JTDDataDef<V, D>
: never
14 changes: 11 additions & 3 deletions lib/vocabularies/discriminator/index.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ import type {CodeKeywordDefinition, AnySchemaObject, KeywordErrorDefinition} fro
import type {KeywordCxt} from "../../compile/validate"
import {_, getProperty, Name} from "../../compile/codegen"
import {DiscrError, DiscrErrorObj} from "../discriminator/types"
import {resolveRef, SchemaEnv} from "../../compile"
import {schemaHasRulesButRef} from "../../compile/util"

export type DiscriminatorError = DiscrErrorObj<DiscrError.Tag> | DiscrErrorObj<DiscrError.Mapping>

@@ -62,10 +64,16 @@ const def: CodeKeywordDefinition = {
const topRequired = hasRequired(parentSchema)
let tagRequired = true
for (let i = 0; i < oneOf.length; i++) {
const sch = oneOf[i]
const propSch = sch.properties?.[tagName]
let sch = oneOf[i]
if (sch?.$ref && !schemaHasRulesButRef(sch, it.self.RULES)) {
sch = resolveRef.call(it.self, it.schemaEnv, it.baseId, sch?.$ref)
if (sch instanceof SchemaEnv) sch = sch.schema
}
const propSch = sch?.properties?.[tagName]
if (typeof propSch != "object") {
throw new Error(`discriminator: oneOf schemas must have "properties/${tagName}"`)
throw new Error(
`discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`
)
}
tagRequired = tagRequired && (topRequired || hasRequired(sch))
addMappings(propSch, i)
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "8.8.2",
"version": "8.9.0",
"description": "Another JSON Schema Validator",
"main": "dist/ajv.js",
"types": "dist/ajv.d.ts",
@@ -71,7 +71,7 @@
"@rollup/plugin-typescript": "^8.2.1",
"@types/chai": "^4.2.12",
"@types/mocha": "^9.0.0",
"@types/node": "^16.3.2",
"@types/node": "^17.0.0",
"@types/require-from-string": "^1.2.0",
"@typescript-eslint/eslint-plugin": "^3.8.0",
"@typescript-eslint/parser": "^3.8.0",
@@ -92,8 +92,9 @@
"karma": "^6.0.0",
"karma-chrome-launcher": "^3.0.0",
"karma-mocha": "^2.0.0",
"lint-staged": "^11.0.0",
"lint-staged": "^12.1.1",
"mocha": "^9.0.2",
"module-from-string": "^3.1.3",
"node-fetch": "^3.0.0",
"nyc": "^15.0.0",
"prettier": "^2.3.1",
80 changes: 79 additions & 1 deletion spec/discriminator.spec.ts
Original file line number Diff line number Diff line change
@@ -81,6 +81,84 @@ describe("discriminator keyword", function () {
})
})

describe("validation with referenced schemas", () => {
const definitions1 = {
schema1: {
properties: {
foo: {const: "x"},
a: {type: "string"},
},
required: ["foo", "a"],
},
schema2: {
properties: {
foo: {enum: ["y", "z"]},
b: {type: "string"},
},
required: ["foo", "b"],
},
}
const mainSchema1 = {
type: "object",
discriminator: {propertyName: "foo"},
oneOf: [
{
$ref: "#/definitions/schema1",
},
{
$ref: "#/definitions/schema2",
},
],
}

const definitions2 = {
schema1: {
properties: {
foo: {const: "x"},
a: {type: "string"},
},
required: ["a"],
},
schema2: {
properties: {
foo: {enum: ["y", "z"]},
b: {type: "string"},
},
required: ["b"],
},
}
const mainSchema2 = {
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
oneOf: [
{
$ref: "#/definitions/schema1",
},
{
$ref: "#/definitions/schema2",
},
],
}

const schema = [
{definitions: definitions1, ...mainSchema1},
{definitions: definitions2, ...mainSchema2},
]

it("should validate data", () => {
assertValid(schema, {foo: "x", a: "a"})
assertValid(schema, {foo: "y", b: "b"})
assertValid(schema, {foo: "z", b: "b"})
assertInvalid(schema, {})
assertInvalid(schema, {foo: 1})
assertInvalid(schema, {foo: "bar"})
assertInvalid(schema, {foo: "x", b: "b"})
assertInvalid(schema, {foo: "y", a: "a"})
assertInvalid(schema, {foo: "z", a: "a"})
})
})

describe("valid schemas", () => {
it("should have oneOf", () => {
invalidSchema(
@@ -97,7 +175,7 @@ describe("discriminator keyword", function () {
required: ["foo"],
oneOf: [{properties: {}}],
},
/discriminator: oneOf schemas must have "properties\/foo"/
/discriminator: oneOf subschemas \(or referenced schemas\) must have "properties\/foo"/
)
})

89 changes: 84 additions & 5 deletions spec/standalone.spec.ts
Original file line number Diff line number Diff line change
@@ -4,8 +4,29 @@ import _Ajv from "./ajv"
import standaloneCode from "../dist/standalone"
import ajvFormats from "ajv-formats"
import requireFromString = require("require-from-string")
import {importFromStringSync} from "module-from-string"
import assert = require("assert")

function testExportTypeEsm(moduleCode: string, singleExport: boolean) {
//Must have
assert.strictEqual(moduleCode.includes("export const"), true)
if (singleExport) {
assert.strictEqual(moduleCode.includes("export default"), true)
}
//Must not have
assert.strictEqual(moduleCode.includes("module.exports"), false)
}
function testExportTypeCjs(moduleCode: string, singleExport: boolean) {
//Must have
if (singleExport) {
assert.strictEqual(moduleCode.includes("module.exports"), true)
} else {
assert.strictEqual(moduleCode.includes("exports.") || moduleCode.includes("exports["), true)
}
//Must not have
assert.strictEqual(moduleCode.includes("export const"), false)
}

describe("standalone code generation", () => {
describe("multiple exports", () => {
let ajv: Ajv
@@ -21,31 +42,68 @@ describe("standalone code generation", () => {
}

describe("without schema keys", () => {
beforeEach(() => {
it("should generate module code with named export - CJS", () => {
ajv = new _Ajv({code: {source: true}})
ajv.addSchema(numSchema)
ajv.addSchema(strSchema)
const moduleCode = standaloneCode(ajv, {
validateNumber: "https://example.com/number.json",
validateString: "https://example.com/string.json",
})
testExportTypeCjs(moduleCode, false)
const m = requireFromString(moduleCode)
assert.strictEqual(Object.keys(m).length, 2)
testExports(m)
})

it("should generate module code with named exports", () => {
it("should generate module code with named export - ESM", () => {
ajv = new _Ajv({code: {source: true, esm: true}})
ajv.addSchema(numSchema)
ajv.addSchema(strSchema)
const moduleCode = standaloneCode(ajv, {
validateNumber: "https://example.com/number.json",
validateString: "https://example.com/string.json",
})
const m = requireFromString(moduleCode)
testExportTypeEsm(moduleCode, false)
const m = importFromStringSync(moduleCode)
assert.strictEqual(Object.keys(m).length, 2)
testExports(m)
})

it("should generate module code with all exports", () => {
it("should generate module code with all exports - CJS", () => {
ajv = new _Ajv({code: {source: true}})
ajv.addSchema(numSchema)
ajv.addSchema(strSchema)
const moduleCode = standaloneCode(ajv)
testExportTypeCjs(moduleCode, false)
const m = requireFromString(moduleCode)
assert.strictEqual(Object.keys(m).length, 2)
testExports({
validateNumber: m["https://example.com/number.json"],
validateString: m["https://example.com/string.json"],
})
})

it("should generate module code with all exports - ESM", () => {
ajv = new _Ajv({code: {source: true, esm: true}})
ajv.addSchema(numSchema)
ajv.addSchema(strSchema)

try {
standaloneCode(ajv)
} catch (err) {
if (err instanceof Error) {
const isMappingErr =
`CodeGen: invalid export name: ${numSchema.$id}, use explicit $id name mapping` ===
err.message ||
`CodeGen: invalid export name: ${strSchema.$id}, use explicit $id name mapping` ===
err.message
assert.strictEqual(isMappingErr, true)
} else {
throw err
}
}
})
})

describe("with schema keys", () => {
@@ -223,13 +281,14 @@ describe("standalone code generation", () => {
}
})

it("should generate module code with a single export (ESM compatible)", () => {
it("should generate module code with a single export - CJS", () => {
const ajv = new _Ajv({code: {source: true}})
const v = ajv.compile({
type: "number",
minimum: 0,
})
const moduleCode = standaloneCode(ajv, v)
testExportTypeCjs(moduleCode, true)
const m = requireFromString(moduleCode)
testExport(m)
testExport(m.default)
@@ -242,6 +301,26 @@ describe("standalone code generation", () => {
}
})

it("should generate module code with a single export - ESM", () => {
const ajv = new _Ajv({code: {source: true, esm: true}})
const v = ajv.compile({
type: "number",
minimum: 0,
})
const moduleCode = standaloneCode(ajv, v)
testExportTypeEsm(moduleCode, true)
const m = importFromStringSync(moduleCode)
testExport(m.validate)
testExport(m.default)

function testExport(validate: AnyValidateFunction<unknown>) {
assert.strictEqual(validate(1), true)
assert.strictEqual(validate(0), true)
assert.strictEqual(validate(-1), false)
assert.strictEqual(validate("1"), false)
}
})

describe("standalone code with ajv-formats", () => {
const schema = {
$schema: "http://json-schema.org/draft-07/schema#",