Skip to content

Commit

Permalink
Merge branch 'ajv-validator:master' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
UNIDY2002 authored Dec 26, 2024
2 parents 78f8213 + 82735a1 commit 265a3c8
Show file tree
Hide file tree
Showing 20 changed files with 214 additions and 41 deletions.
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ Add 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 must start with a letter, `_` or `$`, and may continue with letters, numbers, `_`, `$`, or `-`.
Keyword must start with an ASCII letter, `_` or `$`, and may continue with ASCII letters, numbers, `_`, `$`, `-`, or `:`.
It is recommended to use an application-specific prefix for keywords to avoid current and future name collisions.

Example Keywords:
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/managing-schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ app.post("/user", async (cxt) => {
</code-group>

::: warning Use single Ajv instance
It recommended to use a single Ajv instance for the whole application, so if you use validation in more than one module, you should:
It is recommended to use a single Ajv instance for the whole application, so if you use validation in more than one module, you should:

- require Ajv in a separate module responsible for validation
- compile all validators there
Expand Down Expand Up @@ -186,7 +186,7 @@ In the example above, the key passed to the `addSchema` method was used to retri

### Pre-adding all schemas vs adding on demand

In the example above all schemas were added in advance. It is also possible, to add schemas as they are used - it can be helpful if there is many schemas. In this case, you need to check first whether the schema is already added by calling `getSchema` method - it would return `undefined` if not:
In the example above all schemas were added in advance. It is also possible, to add schemas as they are used - it can be helpful if there are many schemas. In this case, you need to check first whether the schema is already added by calling `getSchema` method - it would return `undefined` if not:

```javascript
const schema_user = require("./schema_user.json")
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/modifying-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ With `useDefaults` option `default` keywords throws exception during schema comp

The strict mode option can change the behaviour for these unsupported defaults (`strict: false` to ignore them, `"log"` to log a warning).

See [Strict mode](./strict-mode.md).
See [Strict mode](../strict-mode.md).

::: tip Default with discriminator keyword
Defaults will be assigned in schemas inside `oneOf` in case [discriminator](../json-schema.md#discriminator) keyword is used.
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/schema-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Draft-2019-09 support is provided via a separate export in order to avoid increa
With this import Ajv supports the following features:

- keywords [`unevaluatedProperties`](../json-schema.md#unevaluatedproperties) and [`unevaluatedItems`](../json-schema.md#unevaluateditems)
- keywords [`dependentRequired`](../json-schema.md#dependentrequired), [`dependentSchemas`](../json-schema.md#dependentschemas), [`maxContains`/`minContain`](../json-schema.md#maxcontains--mincontains)
- keywords [`dependentRequired`](../json-schema.md#dependentrequired), [`dependentSchemas`](../json-schema.md#dependentschemas), [`maxContains`/`minContains`](../json-schema.md#maxcontains-mincontains)
- dynamic recursive references with [`recursiveAnchor`/`recursiveReference`] - see [Extending recursive schemas](./combining-schemas.md#extending-recursive-schemas)
- draft-2019-09 meta-schema is the default.

Expand Down
6 changes: 3 additions & 3 deletions docs/json-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,11 +478,11 @@ To create and equivalent schema in draft-2020-12 use keywords [prefixItems](#pre

The value of the keyword should be a boolean or an object.

If `items` keyword is not present or it is an object, `additionalItems` keyword should be ignored regardless of its value. By default Ajv will throw exception in this case - see [Strict mode](./strict-mode.md)
`additionalItems` keyword is ignored if `items` keyword is not present or is an object. By default Ajv will throw exception in this case - see [Strict mode](./strict-mode.md)

If `items` keyword is an array and data array has not more items than the length of `items` keyword value, `additionalItems` keyword is also ignored.
`additionalItems` keyword is ignored if `items` keyword has more elements than data array.

If the length of data array is bigger than the length of "items" keyword value than the result of the validation depends on the value of `additionalItems` keyword:
If the data array has more elements than the `items` keyword value then the result of the validation depends on the value of `additionalItems` keyword:

- `false`: data is invalid
- `true`: data is valid
Expand Down
15 changes: 14 additions & 1 deletion docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,19 @@ Defines how date-time strings are parsed and validated. By default Ajv only allo
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.
:::

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

Defines how special case numbers `Infinity`, `-Infinity` and `NaN` are handled.

Option values:

- `"fast"` - (default): Do not treat special numbers differently to normal numbers. This is the fastest method but also can produce invalid JSON if the data contains special numbers.
- `"null"` - Special numbers will be serialized to `null` which is the correct behavior according to the JSON spec and is also the same behavior as `JSON.stringify`.

::: warning The default behavior can produce invalid JSON
Using `specialNumbers: "fast" or undefined` can produce invalid JSON when there are any special case numbers in the data.
:::

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

Can be used to disable range checking for `int32` and `uint32` types.
Expand Down Expand Up @@ -344,7 +357,7 @@ Include human-readable messages in errors. `true` by default. `false` can be pas

### uriResolver

By default `uriResolver` is undefined and relies on the embedded uriResolver [uri-js](https://github.com/garycourt/uri-js). Pass an object that satisfies the interface [UriResolver](https://github.com/ajv-validator/ajv/blob/master/lib/types/index.ts) to be used in replacement. One alternative is [fast-uri](https://github.com/fastify/fast-uri).
By default `uriResolver` is undefined and relies on the embedded uriResolver [fast-uri](https://github.com/fastify/fast-uri). Pass an object that satisfies the interface [UriResolver](https://github.com/ajv-validator/ajv/blob/master/lib/types/index.ts) to be used in replacement. One alternative is [uri-js](https://github.com/garycourt/uri-js).

### code <Badge text="v7" />

Expand Down
4 changes: 2 additions & 2 deletions docs/strict-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ By default Ajv fails schema compilation when unknown keywords are used. Users ca
ajv.addKeyword("allowedKeyword")
```

or
or use the convenience method `addVocabulary` for multiple keywords

```javascript
ajv.addVocabulary(["allowed1", "allowed2"])
ajv.addVocabulary(["allowed1", "allowed2"]) // simply calls addKeyword multiple times
```

#### Ignored "additionalItems" keyword
Expand Down
1 change: 1 addition & 0 deletions lib/compile/codegen/code.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export abstract class _CodeOrName {
abstract readonly str: string
abstract readonly names: UsedNames
Expand Down
4 changes: 2 additions & 2 deletions lib/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import N from "./names"
import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve"
import {schemaHasRulesButRef, unescapeFragment} from "./util"
import {validateFunctionCode} from "./validate"
import * as URI from "uri-js"
import {URIComponent} from "fast-uri"
import {JSONType} from "./rules"

export type SchemaRefs = {
Expand Down Expand Up @@ -295,7 +295,7 @@ const PREVENT_SCOPE_CHANGE = new Set([

function getJsonPointer(
this: Ajv,
parsedRef: URI.URIComponents,
parsedRef: URIComponent,
{baseId, schema, root}: SchemaEnv
): SchemaEnv | undefined {
if (parsedRef.fragment?.[0] !== "/") return
Expand Down
15 changes: 13 additions & 2 deletions lib/compile/jtd/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,19 @@ function serializeString({gen, data}: SerializeCxt): void {
gen.add(N.json, _`${useFunc(gen, quote)}(${data})`)
}

function serializeNumber({gen, data}: SerializeCxt): void {
gen.add(N.json, _`"" + ${data}`)
function serializeNumber({gen, data, self}: SerializeCxt): void {
const condition = _`${data} === Infinity || ${data} === -Infinity || ${data} !== ${data}`

if (self.opts.specialNumbers === undefined || self.opts.specialNumbers === "fast") {
gen.add(N.json, _`"" + ${data}`)
} else {
// specialNumbers === "null"
gen.if(
condition,
() => gen.add(N.json, _`null`),
() => gen.add(N.json, _`"" + ${data}`)
)
}
}

function serializeRef(cxt: SerializeCxt): void {
Expand Down
4 changes: 2 additions & 2 deletions lib/compile/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {AnySchema, AnySchemaObject, UriResolver} from "../types"
import type Ajv from "../ajv"
import type {URIComponents} from "uri-js"
import type {URIComponent} from "fast-uri"
import {eachItem} from "./util"
import * as equal from "fast-deep-equal"
import * as traverse from "json-schema-traverse"
Expand Down Expand Up @@ -73,7 +73,7 @@ export function getFullPath(resolver: UriResolver, id = "", normalize?: boolean)
return _getFullPath(resolver, p)
}

export function _getFullPath(resolver: UriResolver, p: URIComponents): string {
export function _getFullPath(resolver: UriResolver, p: URIComponent): string {
const serialized = resolver.serialize(p)
return serialized.split("#")[0] + "#"
}
Expand Down
1 change: 1 addition & 0 deletions lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export interface CurrentOptions {
timestamp?: "string" | "date" // JTD only
parseDate?: boolean // JTD only
allowDate?: boolean // JTD only
specialNumbers?: "fast" | "null" // JTD only
$comment?:
| true
| ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown)
Expand Down
2 changes: 1 addition & 1 deletion lib/runtime/uri.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as uri from "uri-js"
import * as uri from "fast-uri"

type URI = typeof uri & {code: string}
;(uri as URI).code = 'require("ajv/dist/runtime/uri").default'
Expand Down
6 changes: 3 additions & 3 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as URI from "uri-js"
import {URIComponent} from "fast-uri"
import type {CodeGen, Code, Name, ScopeValueSets, ValueScopeName} from "../compile/codegen"
import type {SchemaEnv, SchemaCxt, SchemaObjCxt} from "../compile"
import type {JSONType} from "../compile/rules"
Expand Down Expand Up @@ -238,7 +238,7 @@ export interface RegExpLike {
}

export interface UriResolver {
parse(uri: string): URI.URIComponents
parse(uri: string): URIComponent
resolve(base: string, path: string): string
serialize(component: URI.URIComponents): string
serialize(component: URIComponent): string
}
20 changes: 10 additions & 10 deletions lib/types/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,25 +108,25 @@ type UncheckedJSONSchemaType<T, IsPartial extends boolean> = (
: UncheckedPropertiesSchema<T>
patternProperties?: Record<string, UncheckedJSONSchemaType<T[string], false>>
propertyNames?: Omit<UncheckedJSONSchemaType<string, false>, "type"> & {type?: "string"}
dependencies?: {[K in keyof T]?: Readonly<(keyof T)[]> | UncheckedPartialSchema<T>}
dependentRequired?: {[K in keyof T]?: Readonly<(keyof T)[]>}
dependencies?: {[K in keyof T]?: readonly (keyof T)[] | UncheckedPartialSchema<T>}
dependentRequired?: {[K in keyof T]?: readonly (keyof T)[]}
dependentSchemas?: {[K in keyof T]?: UncheckedPartialSchema<T>}
minProperties?: number
maxProperties?: number
} & (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)[]>}
? {required: readonly (keyof T)[]}
: [UncheckedRequiredMembers<T>] extends [never]
? {required?: Readonly<UncheckedRequiredMembers<T>[]>}
: {required: Readonly<UncheckedRequiredMembers<T>[]>})
? {required?: readonly UncheckedRequiredMembers<T>[]}
: {required: readonly UncheckedRequiredMembers<T>[]})
: T extends null
? {
type: JSONType<"null", IsPartial>
nullable: true
}
: never) & {
allOf?: Readonly<UncheckedPartialSchema<T>[]>
anyOf?: Readonly<UncheckedPartialSchema<T>[]>
oneOf?: Readonly<UncheckedPartialSchema<T>[]>
allOf?: readonly UncheckedPartialSchema<T>[]
anyOf?: readonly UncheckedPartialSchema<T>[]
oneOf?: readonly UncheckedPartialSchema<T>[]
if?: UncheckedPartialSchema<T>
then?: UncheckedPartialSchema<T>
else?: UncheckedPartialSchema<T>
Expand Down Expand Up @@ -176,12 +176,12 @@ type Nullable<T> = undefined extends T
? {
nullable: true
const?: null // any non-null value would fail `const: null`, `null` would fail any other value in const
enum?: Readonly<(T | null)[]> // `null` must be explicitly included in "enum" for `null` to pass
enum?: readonly (T | null)[] // `null` must be explicitly included in "enum" for `null` to pass
default?: T | null
}
: {
nullable?: false
const?: T
enum?: Readonly<T[]>
enum?: readonly T[]
default?: T
}
5 changes: 4 additions & 1 deletion lib/vocabularies/discriminator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {KeywordCxt} from "../../compile/validate"
import {_, getProperty, Name} from "../../compile/codegen"
import {DiscrError, DiscrErrorObj} from "../discriminator/types"
import {resolveRef, SchemaEnv} from "../../compile"
import MissingRefError from "../../compile/ref_error"
import {schemaHasRulesButRef} from "../../compile/util"

export type DiscriminatorError = DiscrErrorObj<DiscrError.Tag> | DiscrErrorObj<DiscrError.Mapping>
Expand Down Expand Up @@ -66,8 +67,10 @@ const def: CodeKeywordDefinition = {
for (let i = 0; i < oneOf.length; i++) {
let sch = oneOf[i]
if (sch?.$ref && !schemaHasRulesButRef(sch, it.self.RULES)) {
sch = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, sch?.$ref)
const ref = sch.$ref
sch = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, ref)
if (sch instanceof SchemaEnv) sch = sch.schema
if (sch === undefined) throw new MissingRefError(it.opts.uriResolver, it.baseId, ref)
}
const propSch = sch?.properties?.[tagName]
if (typeof propSch != "object") {
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "8.13.0",
"version": "8.17.1",
"description": "Another JSON Schema Validator",
"main": "dist/ajv.js",
"types": "dist/ajv.d.ts",
Expand All @@ -9,6 +9,7 @@
"dist/",
".runkit_example.js"
],
"sideEffects": false,
"scripts": {
"eslint": "eslint \"lib/**/*.ts\" \"spec/**/*.*s\" --ignore-pattern spec/JSON-Schema-Test-Suite",
"prettier:write": "prettier --write \"./**/*.{json,yaml,js,ts}\"",
Expand Down Expand Up @@ -59,9 +60,9 @@
"runkitExampleFilename": ".runkit_example.js",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.4.1"
"require-from-string": "^2.0.2"
},
"devDependencies": {
"@ajv-validator/config": "^0.5.0",
Expand All @@ -83,7 +84,6 @@
"dayjs-plugin-utc": "^0.1.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"fast-uri": "^2.3.0",
"glob": "^10.3.10",
"husky": "^9.0.11",
"if-node-version": "^1.1.1",
Expand All @@ -104,7 +104,8 @@
"rollup-plugin-terser": "^7.0.2",
"ts-node": "^10.9.2",
"tsify": "^5.0.4",
"typescript": "5.3.3"
"typescript": "5.3.3",
"uri-js": "^4.4.1"
},
"collective": {
"type": "opencollective",
Expand Down
61 changes: 61 additions & 0 deletions spec/discriminator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,67 @@ describe("discriminator keyword", function () {
})
})

describe("schema with external $refs", () => {
const schemas = {
main: {
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
oneOf: [
{
$ref: "schema1",
},
{
$ref: "schema2",
},
],
},
schema1: {
type: "object",
properties: {
foo: {const: "x"},
},
},
schema2: {
type: "object",
properties: {
foo: {enum: ["y", "z"]},
},
},
}

const data = {foo: "x"}
const badData = {foo: "w"}

it("compile should resolve each $ref to a schema that was added with addSchema", () => {
const opts = {
discriminator: true,
}
const ajv = new _Ajv(opts)
ajv.addSchema(schemas.main, "https://host/main")
ajv.addSchema(schemas.schema1, "https://host/schema1")
ajv.addSchema(schemas.schema2, "https://host/schema2")

const validate = ajv.compile({$ref: "https://host/main"})
assert.strictEqual(validate(data), true)
assert.strictEqual(validate(badData), false)
})
it("compileAsync should loadSchema each $ref", async () => {
const opts = {
discriminator: true,
loadSchema(url) {
if (!url.startsWith("https://host/")) return undefined
const name = url.substring("https://host/".length)
return schemas[name]
},
}
const ajv = new _Ajv(opts)
const validate = await ajv.compileAsync({$ref: "https://host/main"})
assert.strictEqual(validate(data), true)
assert.strictEqual(validate(badData), false)
})
})

describe("validation with deeply referenced schemas", () => {
const schema = [
{
Expand Down
Loading

0 comments on commit 265a3c8

Please sign in to comment.